diff --git a/miette-derive/src/code.rs b/miette-derive/src/code.rs index f607f49..c1edd17 100644 --- a/miette-derive/src/code.rs +++ b/miette-derive/src/code.rs @@ -6,7 +6,7 @@ use syn::{ Token, }; -use crate::diagnostic::{Diagnostic, DiagnosticVariant}; +use crate::diagnostic::DiagnosticVariant; #[derive(Debug)] pub struct Code(pub String); @@ -44,10 +44,7 @@ impl Parse for Code { } impl Code { - pub(crate) fn gen_enum( - _diag: &Diagnostic, - variants: &[DiagnosticVariant], - ) -> Option { + pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option { let code_pairs = variants.iter().map( |DiagnosticVariant { ref ident, diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index 790a097..8e9a47a 100644 --- a/miette-derive/src/diagnostic.rs +++ b/miette-derive/src/diagnostic.rs @@ -6,6 +6,7 @@ use crate::code::Code; use crate::diagnostic_arg::DiagnosticArg; use crate::help::Help; use crate::severity::Severity; +use crate::snippets::Snippets; pub enum Diagnostic { Struct { @@ -14,6 +15,7 @@ pub enum Diagnostic { code: Code, severity: Option, help: Option, + snippets: Option, }, Enum { ident: syn::Ident, @@ -28,12 +30,13 @@ pub struct DiagnosticVariant { pub code: Code, pub severity: Option, pub help: Option, + pub snippets: Option, } impl Diagnostic { pub fn from_derive_input(input: DeriveInput) -> Result { Ok(match input.data { - syn::Data::Struct(_) => { + syn::Data::Struct(data_struct) => { if let Some(attr) = input.attrs.iter().find(|x| x.path.is_ident("diagnostic")) { let args = attr.parse_args_with( Punctuated::::parse_terminated, @@ -53,6 +56,7 @@ impl Diagnostic { DiagnosticArg::Help(hl) => help = Some(hl), } } + let snippets = Snippets::from_fields(&data_struct.fields)?; let ident = input.ident.clone(); Diagnostic::Struct { ident: input.ident, @@ -62,6 +66,7 @@ impl Diagnostic { })?, help, severity, + snippets, } } else { // Also handle when there's multiple `#[diagnostic]` attrs? @@ -95,6 +100,7 @@ impl Diagnostic { } } } + let snippets = Snippets::from_fields(&var.fields)?; let ident = input.ident.clone(); vars.push(DiagnosticVariant { ident: var.ident, @@ -104,6 +110,7 @@ impl Diagnostic { })?, help, severity, + snippets, }); } else { // Also handle when there's multiple `#[diagnostic]` attrs? @@ -136,17 +143,20 @@ impl Diagnostic { code, severity, help, + snippets, } => { let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); let code_body = code.gen_struct(); let help_body = help.as_ref().and_then(|x| x.gen_struct()); let sev_body = severity.as_ref().and_then(|x| x.gen_struct()); + let snip_body = snippets.as_ref().and_then(|x| x.gen_struct()); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { #code_body #help_body #sev_body + #snip_body } } } @@ -156,15 +166,17 @@ impl Diagnostic { variants, } => { let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); - let code_body = Code::gen_enum(self, variants); - let help_body = Help::gen_enum(self, variants); - let sev_body = Severity::gen_enum(self, variants); + let code_body = Code::gen_enum(variants); + let help_body = Help::gen_enum(variants); + let sev_body = Severity::gen_enum(variants); + let snip_body = Snippets::gen_enum(variants); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { #code_body #help_body #sev_body + #snip_body } } } diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs index 22e98b6..10dde3e 100644 --- a/miette-derive/src/help.rs +++ b/miette-derive/src/help.rs @@ -6,7 +6,7 @@ use syn::{ Token, }; -use crate::diagnostic::{Diagnostic, DiagnosticVariant}; +use crate::diagnostic::DiagnosticVariant; pub struct Help { pub fmt: String, @@ -65,10 +65,7 @@ impl Parse for Help { } } impl Help { - pub(crate) fn gen_enum( - _diag: &Diagnostic, - variants: &[DiagnosticVariant], - ) -> Option { + pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option { let help_pairs = variants .iter() .filter(|v| v.help.is_some()) diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index a88d868..72f2f64 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -8,8 +8,9 @@ mod diagnostic; mod diagnostic_arg; mod help; mod severity; +mod snippets; -#[proc_macro_derive(Diagnostic, attributes(diagnostic))] +#[proc_macro_derive(Diagnostic, attributes(diagnostic, snippet, highlight))] pub fn derive_diagnostic(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let cmd = match Diagnostic::from_derive_input(input) { diff --git a/miette-derive/src/severity.rs b/miette-derive/src/severity.rs index c585eef..df9b562 100644 --- a/miette-derive/src/severity.rs +++ b/miette-derive/src/severity.rs @@ -6,7 +6,7 @@ use syn::{ Token, }; -use crate::diagnostic::{Diagnostic, DiagnosticVariant}; +use crate::diagnostic::DiagnosticVariant; pub struct Severity(pub syn::Path); @@ -36,10 +36,7 @@ impl Parse for Severity { } } impl Severity { - pub(crate) fn gen_enum( - _diag: &Diagnostic, - variants: &[DiagnosticVariant], - ) -> Option { + pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option { let sev_pairs = variants .iter() .filter(|v| v.severity.is_some()) diff --git a/miette-derive/src/snippets.rs b/miette-derive/src/snippets.rs new file mode 100644 index 0000000..10f9639 --- /dev/null +++ b/miette-derive/src/snippets.rs @@ -0,0 +1,409 @@ +use std::collections::HashMap; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Token, +}; + +use crate::diagnostic::DiagnosticVariant; + +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 { + Member(syn::Member), + String(syn::LitStr), +} + +impl ToTokens for MemberOrString { + fn to_tokens(&self, tokens: &mut TokenStream) { + use MemberOrString::*; + match self { + Member(member) => member.to_tokens(tokens), + String(string) => string.to_tokens(tokens), + } + } +} + +impl Parse for MemberOrString { + fn parse(input: ParseStream) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(syn::Ident) || lookahead.peek(syn::LitInt) { + Ok(MemberOrString::Member(input.parse()?)) + } else if lookahead.peek(syn::LitStr) { + Ok(MemberOrString::String(input.parse()?)) + } else { + Err(syn::Error::new( + input.span(), + "Expected a string or a field reference.", + )) + } + } +} + +impl Parse for SnippetAttr { + fn parse(input: ParseStream) -> syn::Result { + let punc = Punctuated::::parse_terminated(input)?; + let span = input.span(); + let mut iter = punc.into_iter(); + let source = match iter.next() { + Some(MemberOrString::Member(member)) => member, + _ => { + return Err(syn::Error::new( + span, + "Source must be an identifier that refers to a Source for this snippet.", + )) + } + }; + 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, + }) + } +} + +impl Parse for HighlightAttr { + fn parse(input: ParseStream) -> syn::Result { + let punc = Punctuated::::parse_terminated(input)?; + let span = input.span(); + let mut iter = punc.into_iter(); + let snippet = + match iter.next() { + Some(MemberOrString::Member(member)) => member, + _ => return Err(syn::Error::new( + span, + "must be an identifier that refers to something with a #[snippet] attribute.", + )), + }; + let label = iter.next(); + Ok(HighlightAttr { snippet, label }) + } +} + +impl Snippets { + pub fn from_fields(fields: &syn::Fields) -> syn::Result> { + match fields { + syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()), + syn::Fields::Unnamed(unnamed) => { + Self::from_fields_vec(unnamed.unnamed.iter().collect()) + } + syn::Fields::Unit => Ok(None), + } + } + + fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result> { + let mut snippets = HashMap::new(); + // First we collect all the contexts + for (i, field) in fields.iter().enumerate() { + for attr in &field.attrs { + if attr.path.is_ident("snippet") { + let snippet = if let Some(ident) = field.ident.clone() { + syn::Member::Named(ident) + } else { + syn::Member::Unnamed(syn::Index { + index: i as u32, + span: field.span(), + }) + }; + let SnippetAttr { + source, + message, + source_name, + } = 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, + }, + ); + } + } + } + // Then we loop again looking for highlights + 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::()?; + if let Some(snippet) = snippets.get_mut(&snippet) { + let member = if let Some(ident) = field.ident.clone() { + syn::Member::Named(ident) + } else { + syn::Member::Unnamed(syn::Index { + index: i as u32, + span: field.span(), + }) + }; + snippet.highlights.push(Highlight { + label, + highlight: member, + }); + } else { + return Err(syn::Error::new(snippet.span(), "Highlight must refer to an existing field with a #[snippet(...)] attribute.")); + } + } + } + } + if snippets.is_empty() { + Ok(None) + } else { + Ok(Some(Snippets(snippets.into_values().collect()))) + } + } + + pub(crate) fn gen_struct(&self) -> Option { + let snippets = self.0.iter().map(|snippet| { + // snippet message + let msg = snippet + .message + .as_ref() + .map(|msg| match msg { + MemberOrString::String(str) => { + quote! { + message: std::option::Option::Some(#str.into()), + } + } + MemberOrString::Member(m) => { + quote! { + message: std::option::Option::Some(self.#m.clone()), + } + } + }) + .unwrap_or_else(|| { + quote! { + message: std::option::Option::None, + } + }); + + // Source field + let src_ident = &snippet.source; + let src_ident = quote! { + // TODO: I don't like this. Think about it more and maybe improve protocol? + 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! { + context: self.#context.clone(), + }; + + // Highlights + let highlights = snippet.highlights.iter().map(|highlight| { + let Highlight { highlight, label } = highlight; + quote! { + (#label.into(), self.#highlight.clone()) + } + }); + let highlights = quote! { + highlights: std::option::Option::Some(vec![ + #(#highlights),* + ]), + }; + + // Generate the snippet itself + quote! { + miette::DiagnosticSnippet { + #msg + #src_name + #src_ident + #context + #highlights + } + } + }); + Some(quote! { + fn snippets(&self) -> std::option::Option>> { + Some(Box::new(vec![ + #(#snippets),* + ].into_iter())) + } + }) + } + + pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option { + let variant_arms = variants.iter().map(|variant| { + variant.snippets.as_ref().map(|snippets| { + let variant_snippets = snippets.0.iter().map(|snippet| { + // snippet message + let msg = snippet + .message + .as_ref() + .map(|msg| match msg { + MemberOrString::String(str) => { + quote! { + message: std::option::Option::Some(#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! { + message: std::option::Option::Some(#m.clone()), + } + } + }) + .unwrap_or_else(|| { + quote! { + message: std::option::Option::None, + } + }); + + // Source field + let src_ident = match &snippet.source { + syn::Member::Named(id) => id.clone(), + syn::Member::Unnamed(syn::Index { index, .. }) => { + format_ident!("_{}", index) + } + }; + let src_ident = quote! { + // TODO: I don't like this. Think about it more and maybe improve protocol? + 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(), + syn::Member::Unnamed(syn::Index { index, .. }) => { + format_ident!("_{}", index) + } + }; + let context = quote! { + context: #context.clone(), + }; + + // Highlights + let highlights = snippet.highlights.iter().map(|highlight| { + let Highlight { highlight, label } = highlight; + let m = match highlight { + syn::Member::Named(id) => id.clone(), + syn::Member::Unnamed(syn::Index { index, .. }) => { + format_ident!("_{}", index) + } + }; + quote! { + (#label.into(), #m.clone()) + } + }); + let highlights = quote! { + highlights: std::option::Option::Some(vec![ + #(#highlights),* + ]), + }; + + // Generate the snippet itself + quote! { + miette::DiagnosticSnippet { + #msg + #src_name + #src_ident + #context + #highlights + } + } + }); + let variant_name = variant.ident.clone(); + let members = variant.fields.iter().enumerate().map(|(i, field)| { + field + .ident + .as_ref() + .cloned() + .unwrap_or_else(|| format_ident!("_{}", i)) + }); + match &variant.fields { + syn::Fields::Unit => None, + syn::Fields::Named(_) => Some(quote! { + Self::#variant_name { #(#members),* } => std::option::Option::Some(std::boxed::Box::new(vec![ + #(#variant_snippets),* + ].into_iter())), + }), + syn::Fields::Unnamed(_) => Some(quote! { + Self::#variant_name(#(#members),*) => std::option::Option::Some(Box::new(vec![ + #(#variant_snippets),* + ].into_iter())), + }), + } + }) + }); + Some(quote! { + fn snippets(&self) -> std::option::Option>> { + match self { + #(#variant_arms)* + _ => std::option::Option::None, + } + } + }) + } +} diff --git a/tests/derive.rs b/tests/derive.rs index bf1ddff..a8d6df2 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -1,4 +1,6 @@ -use miette::{Diagnostic, Severity}; +use std::sync::Arc; + +use miette::{Diagnostic, Severity, SourceSpan}; use thiserror::Error; #[test] @@ -156,3 +158,108 @@ fn fmt_help() { assert_eq!("1 bar".to_string(), FooEnum::X.help().unwrap().to_string()); } + +#[test] +fn test_snippet_named_struct() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic(code(foo::bar::baz))] + struct Foo { + // The actual "source code" our contexts will be using. This can be + // reused by multiple contexts! + // + // The `Arc` is so you don't have to clone the entire thing into this + // Diagnostic. We just need to be able to read it~ + src: Arc, + + // The "snippet" span. This is the span that will be displayed to + // users. It should be a big enough slice of the Source to provide + // reasonable context, but still somewhat compact. + // + // You can have as many of these #[snippet] fields as you want, and + // even feed them from different sources! + // + // Example display: + // / [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, + + // "Highlights" are the specific highlights _inside_ the snippet. + // These will be used to underline/point to specific sections of the + // #[snippet] they refer to. As such, these SourceSpans must be within + // the bounds of their referenced snippet. + // + // Example display: + // 1 | var1 + var2 + // | ^^^^ ^^^^ - var 2 + // | | + // | var 1 + #[highlight(snip, "var 1")] + var1: SourceSpan, + #[highlight(snip, "var 2")] + var2: SourceSpan, + + // Now with member source names + filename: String, + second_message: String, + #[snippet(src, filename, second_message)] + snip2: SourceSpan, + } +} + +#[test] +fn test_snippet_unnamed_struct() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[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, + // referenced source name + String, + String, + #[snippet(0, 4, 5)] SourceSpan, + #[highlight(6, "var 3")] SourceSpan, + #[highlight(6, "var 4")] SourceSpan, + ); +} + +#[test] +fn test_snippet_enum() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[allow(dead_code)] + enum Foo { + #[diagnostic(code(foo::a))] + A { + src: Arc, + #[snippet(src, "my_snippet.rs", "hi this is where the thing went wrong")] + snip: SourceSpan, + #[highlight(snip, "var 1")] + var1: SourceSpan, + #[highlight(snip, "var 2")] + var2: SourceSpan, + filename: String, + second_message: String, + #[snippet(src, filename, second_message)] + snip2: SourceSpan, + }, + #[diagnostic(code(foo::b))] + B( + Arc, + #[snippet(0, "my_snippet.rs", "hi")] SourceSpan, + #[highlight(1, "var 1")] SourceSpan, + #[highlight(1, "var 2")] SourceSpan, + // referenced source name + String, + String, + #[snippet(0, 4, 5)] SourceSpan, + #[highlight(6, "var 3")] SourceSpan, + #[highlight(6, "var 4")] SourceSpan, + ), + } +}