feat(derive): Support for deriving snippet method (#18)

Fixes: #11
This commit is contained in:
Kat Marchán 2021-08-16 22:20:13 -07:00 committed by GitHub
parent 3e5ee0ee4d
commit f6e6acf2d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 541 additions and 21 deletions

View File

@ -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<TokenStream> {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
let code_pairs = variants.iter().map(
|DiagnosticVariant {
ref ident,

View File

@ -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<Severity>,
help: Option<Help>,
snippets: Option<Snippets>,
},
Enum {
ident: syn::Ident,
@ -28,12 +30,13 @@ pub struct DiagnosticVariant {
pub code: Code,
pub severity: Option<Severity>,
pub help: Option<Help>,
pub snippets: Option<Snippets>,
}
impl Diagnostic {
pub fn from_derive_input(input: DeriveInput) -> Result<Self, syn::Error> {
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::<DiagnosticArg, Token![,]>::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
}
}
}

View File

@ -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<TokenStream> {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
let help_pairs = variants
.iter()
.filter(|v| v.help.is_some())

View File

@ -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) {

View File

@ -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<TokenStream> {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
let sev_pairs = variants
.iter()
.filter(|v| v.severity.is_some())

View File

@ -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<Snippet>);
struct Snippet {
message: Option<MemberOrString>,
highlights: Vec<Highlight>,
source_name: MemberOrString,
source: syn::Member,
snippet: syn::Member,
}
struct Highlight {
highlight: syn::Member,
label: Option<MemberOrString>,
}
struct SnippetAttr {
source: syn::Member,
source_name: MemberOrString,
message: Option<MemberOrString>,
}
struct HighlightAttr {
snippet: syn::Member,
label: Option<MemberOrString>,
}
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<Self> {
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<Self> {
let punc = Punctuated::<MemberOrString, Token![,]>::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<Self> {
let punc = Punctuated::<MemberOrString, Token![,]>::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<Option<Self>> {
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<Option<Self>> {
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::<SnippetAttr>()?;
// 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::<HighlightAttr>()?;
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<TokenStream> {
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<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet>>> {
Some(Box::new(vec![
#(#snippets),*
].into_iter()))
}
})
}
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
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<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet>>> {
match self {
#(#variant_arms)*
_ => std::option::Option::None,
}
}
})
}
}

View File

@ -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<String>,
// 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<String>,
#[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<String>,
#[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<String>,
#[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,
),
}
}