From 9a78a943950078c879a1eb06baf819348139e1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sun, 15 Aug 2021 17:27:39 -0700 Subject: [PATCH] fix(derive): move to plain syn to fix darling issues --- miette-derive/Cargo.toml | 1 - miette-derive/src/code.rs | 91 ++++++++------- miette-derive/src/diagnostic.rs | 175 ++++++++++++++++++++++++++++ miette-derive/src/diagnostic_arg.rs | 26 +++++ miette-derive/src/help.rs | 92 ++++++++------- miette-derive/src/lib.rs | 92 +-------------- miette-derive/src/severity.rs | 66 ++++++----- tests/derive.rs | 58 +++++---- 8 files changed, 376 insertions(+), 225 deletions(-) create mode 100644 miette-derive/src/diagnostic.rs create mode 100644 miette-derive/src/diagnostic_arg.rs diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index 27b0c28..33018bc 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -14,4 +14,3 @@ proc-macro = true proc-macro2 = "1.0" quote = "1.0" syn = "1.0.45" -darling = "0.13.0" diff --git a/miette-derive/src/code.rs b/miette-derive/src/code.rs index ebed1ae..f607f49 100644 --- a/miette-derive/src/code.rs +++ b/miette-derive/src/code.rs @@ -1,39 +1,44 @@ -use std::fmt::Display; - -use darling::{ast::Fields, error::Error as DarlingError, FromMeta}; use proc_macro2::TokenStream; use quote::quote; -use syn::{Lit, Meta, NestedMeta}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream}, + Token, +}; -use crate::{Diagnostic, DiagnosticField, DiagnosticVariant}; +use crate::diagnostic::{Diagnostic, DiagnosticVariant}; #[derive(Debug)] -pub struct Code(String); +pub struct Code(pub String); -impl Display for Code { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromMeta for Code { - fn from_string(arg: &str) -> Result { - Ok(Code(arg.into())) - } - - fn from_list(items: &[NestedMeta]) -> Result { - match &items[0] { - NestedMeta::Meta(Meta::Path(p)) => Ok(Code( - p.segments - .iter() - .map(|s| s.ident.to_string()) - .collect::>() - .join("::"), - )), - NestedMeta::Lit(Lit::Str(code)) => Ok(Code(code.value())), - _ => Err(DarlingError::custom( - "invalid code format. Only path::style and string literals are accepted", - )), +impl Parse for Code { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.parse::()?; + if ident == "code" { + let la = input.lookahead1(); + if la.peek(syn::token::Paren) { + let content; + parenthesized!(content in input); + let la = content.lookahead1(); + if la.peek(syn::LitStr) { + let str = content.parse::()?; + Ok(Code(str.value())) + } else { + let path = content.parse::()?; + Ok(Code( + path.segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::"), + )) + } + } else { + input.parse::()?; + Ok(Code(input.parse::()?.value())) + } + } else { + Err(syn::Error::new(ident.span(), "diagnostic code is required. Use #[diagnostic(code = ...)] or #[diagnostic(code(...))] to define one.")) } } } @@ -41,16 +46,25 @@ impl FromMeta for Code { impl Code { pub(crate) fn gen_enum( _diag: &Diagnostic, - variants: &[&DiagnosticVariant], + variants: &[DiagnosticVariant], ) -> Option { let code_pairs = variants.iter().map( |DiagnosticVariant { ref ident, ref code, + ref fields, .. }| { - let code = code.to_string(); - quote! { Self::#ident => std::boxed::Box::new(#code), } + let code = &code.0; + match fields { + syn::Fields::Named(_) => { + quote! { Self::#ident { .. } => std::boxed::Box::new(#code), } + } + syn::Fields::Unnamed(_) => { + quote! { Self::#ident(..) => std::boxed::Box::new(#code), } + } + syn::Fields::Unit => quote! { Self::#ident => std::boxed::Box::new(#code), }, + } }, ); Some(quote! { @@ -62,15 +76,8 @@ impl Code { }) } - pub(crate) fn gen_struct( - diag: &Diagnostic, - _fields: &Fields<&DiagnosticField>, - ) -> Option { - let code = diag - .code - .as_ref() - .expect("`code` attribute is required for diagnostics.") - .to_string(); + pub(crate) fn gen_struct(&self) -> Option { + let code = &self.0; Some(quote! { fn code<'a>(&'a self) -> std::boxed::Box { std::boxed::Box::new(#code) diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs new file mode 100644 index 0000000..8886648 --- /dev/null +++ b/miette-derive/src/diagnostic.rs @@ -0,0 +1,175 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{punctuated::Punctuated, DeriveInput, Token}; + +use crate::code::Code; +use crate::diagnostic_arg::DiagnosticArg; +use crate::help::Help; +use crate::severity::Severity; + +pub enum Diagnostic { + Struct { + ident: syn::Ident, + generics: syn::Generics, + code: Code, + severity: Option, + help: Option, + }, + Enum { + ident: syn::Ident, + generics: syn::Generics, + variants: Vec, + }, +} + +pub struct DiagnosticVariant { + pub ident: syn::Ident, + pub fields: syn::Fields, + pub code: Code, + pub severity: Option, + pub help: Option, +} + +impl Diagnostic { + pub fn from_derive_input(input: DeriveInput) -> Result { + Ok(match input.data { + syn::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, + )?; + let mut code = None; + let mut severity = None; + let mut help = None; + for arg in args { + match arg { + DiagnosticArg::Code(new_code) => { + // TODO: error on multiple? + code = Some(new_code); + } + DiagnosticArg::Severity(sev) => { + severity = Some(sev); + } + DiagnosticArg::Help(hl) => { + help = Some(hl) + } + } + } + let ident = input.ident.clone(); + Diagnostic::Struct { + ident: input.ident, + generics: input.generics, + code: code.ok_or_else(|| { + syn::Error::new(ident.span(), "Diagnostic code is required.") + })?, + help, + severity, + } + } else { + // Also handle when there's multiple `#[diagnostic]` attrs? + return Err(syn::Error::new( + input.ident.span(), + "#[diagnostic] attribute is required when deriving Diagnostic.", + )); + } + } + syn::Data::Enum(syn::DataEnum { variants, .. }) => { + let mut vars = Vec::new(); + for var in variants { + if let Some(attr) = var.attrs.iter().find(|x| x.path.is_ident("diagnostic")) { + let args = attr.parse_args_with( + Punctuated::::parse_terminated, + )?; + let mut code = None; + let mut severity = None; + let mut help = None; + for arg in args { + match arg { + DiagnosticArg::Code(new_code) => { + // TODO: error on multiple? + code = Some(new_code); + } + DiagnosticArg::Severity(sev) => { + severity = Some(sev); + } + DiagnosticArg::Help(hl) => { + help = Some(hl); + } + } + } + let ident = input.ident.clone(); + vars.push(DiagnosticVariant { + ident: var.ident, + fields: var.fields, + code: code.ok_or_else(|| { + syn::Error::new(ident.span(), "Diagnostic code is required.") + })?, + help, + severity, + }); + } else { + // Also handle when there's multiple `#[diagnostic]` attrs? + return Err(syn::Error::new( + var.ident.span(), + "#[diagnostic] attribute is required on all enum variants when deriving Diagnostic.", + )); + } + } + Diagnostic::Enum { + ident: input.ident, + generics: input.generics, + variants: vars, + } + } + syn::Data::Union(_) => { + return Err(syn::Error::new( + input.ident.span(), + "Can't derive Diagnostic for Unions", + )) + } + }) + } + + pub fn gen(&self) -> TokenStream { + match self { + Self::Struct { + ident, + generics, + code, + severity, + help, + } => { + 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()); + + quote! { + impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { + #code_body + #help_body + #sev_body + } + } + } + Self::Enum { + ident, + generics, + 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); + + quote! { + impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { + #code_body + #help_body + #sev_body + } + } + } + } + } +} diff --git a/miette-derive/src/diagnostic_arg.rs b/miette-derive/src/diagnostic_arg.rs new file mode 100644 index 0000000..89fbb42 --- /dev/null +++ b/miette-derive/src/diagnostic_arg.rs @@ -0,0 +1,26 @@ +use syn::parse::{Parse, ParseStream}; + +use crate::code::Code; +use crate::help::Help; +use crate::severity::Severity; + +pub enum DiagnosticArg { + Code(Code), + Severity(Severity), + Help(Help), +} + +impl Parse for DiagnosticArg { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.fork().parse::()?; + if ident == "code" { + Ok(DiagnosticArg::Code(input.parse()?)) + } else if ident == "severity" { + Ok(DiagnosticArg::Severity(input.parse()?)) + } else if ident == "help" { + Ok(DiagnosticArg::Help(input.parse()?)) + } else { + Err(syn::Error::new(ident.span(), "Unrecognized diagnostic option")) + } + } +} diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs index c84b053..a974d40 100644 --- a/miette-derive/src/help.rs +++ b/miette-derive/src/help.rs @@ -1,50 +1,47 @@ -use darling::{ast::Fields, error::Error as DarlingError, FromMeta}; use proc_macro2::TokenStream; use quote::quote; -use syn::{Lit, NestedMeta}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream}, + Token, +}; -use crate::{Diagnostic, DiagnosticField, DiagnosticVariant}; +use crate::diagnostic::{Diagnostic, DiagnosticVariant}; -#[derive(Debug)] pub struct Help { pub fmt: String, - pub args: Vec, + pub args: Vec, } -impl FromMeta for Help { - fn from_string(arg: &str) -> Result { - Ok(Help { - fmt: arg.into(), - args: Vec::new(), - }) - } - - fn from_list(items: &[NestedMeta]) -> Result { - match &items.get(0) { - Some(NestedMeta::Lit(Lit::Str(fmt))) => Ok(Help { - fmt: fmt.value(), - args: items[1..] - .iter() - .map(|item| match item { - NestedMeta::Meta(_) => Err(DarlingError::custom( - "Only literals are supported for now. Sorry :(" - )), - NestedMeta::Lit(_) => Ok(item.clone()), - }) - .collect::, DarlingError>>()?, - }), - None => Err(DarlingError::custom("Help format string is required")), - _ => Err(DarlingError::custom( - "First argument must be a literal format string", - )), +impl Parse for Help { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.parse::()?; + if ident == "help" { + let la = input.lookahead1(); + if la.peek(syn::token::Paren) { + let content; + parenthesized!(content in input); + let str = content.parse::()?; + Ok(Help { + fmt: str.value(), + args: Vec::new(), + }) + } else { + input.parse::()?; + Ok(Help { + fmt: input.parse::()?.value(), + args: Vec::new(), + }) + } + } else { + Err(syn::Error::new(ident.span(), "not a help")) } } } - impl Help { pub(crate) fn gen_enum( _diag: &Diagnostic, - variants: &[&DiagnosticVariant], + variants: &[DiagnosticVariant], ) -> Option { let help_pairs = variants .iter() @@ -53,12 +50,22 @@ impl Help { |DiagnosticVariant { ref ident, ref help, + ref fields, .. }| { let help = &help.as_ref().unwrap(); let fmt = &help.fmt; let args = help.args.iter().map(|arg| quote! { #arg, }); - quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))), } + match fields { + syn::Fields::Named(_) => { + quote! { Self::#ident{..} => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))), } + } + syn::Fields::Unnamed(_) => { + quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))), } + } + syn::Fields::Unit => + quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))), }, + } }, ) .collect::>(); @@ -76,17 +83,12 @@ impl Help { } } - pub(crate) fn gen_struct( - diag: &Diagnostic, - _fields: &Fields<&DiagnosticField>, - ) -> Option { - diag.help.as_ref().map(|h| { - let fmt = &h.fmt; - let args = &h.args; - quote! { - fn help<'a>(&'a self) -> std::option::Option> { - std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))) - } + pub(crate) fn gen_struct(&self) -> Option { + let fmt = &self.fmt; + let args = &self.args; + Some(quote! { + fn help<'a>(&'a self) -> std::option::Option> { + std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))) } }) } diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index 665ab53..a88d868 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -1,101 +1,21 @@ -use darling::{ - ast::{self, Fields}, - FromDeriveInput, FromField, FromVariant, ToTokens, -}; -use proc_macro2::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; -use code::Code; -use help::Help; -use severity::Severity; +use diagnostic::Diagnostic; mod code; +mod diagnostic; +mod diagnostic_arg; mod help; mod severity; #[proc_macro_derive(Diagnostic, attributes(diagnostic))] 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) { - Ok(cmd) => cmd, - Err(err) => return err.write_errors().into(), + let cmd = match Diagnostic::from_derive_input(input) { + Ok(cmd) => cmd.gen(), + Err(err) => return err.to_compile_error().into(), }; // panic!("{:#}", cmd.to_token_stream()); quote!(#cmd).into() } - -#[derive(Debug, FromDeriveInput)] -#[darling(supports(any), attributes(diagnostic))] -struct Diagnostic { - ident: syn::Ident, - data: ast::Data, - generics: syn::Generics, - #[darling(default)] - code: Option, - #[darling(default)] - severity: Option, - #[darling(default)] - help: Option, -} - -#[derive(Debug, FromField)] -struct DiagnosticField { - ident: Option, - ty: syn::Type, -} - -#[derive(Debug, FromVariant)] -#[darling(attributes(diagnostic))] -struct DiagnosticVariant { - ident: syn::Ident, - code: Code, - #[darling(default)] - severity: Option, - #[darling(default)] - help: Option, -} - -impl ToTokens for Diagnostic { - fn to_tokens(&self, tokens: &mut TokenStream) { - let ts = match self.data.as_ref() { - ast::Data::Enum(variants) => self.gen_enum(variants), - ast::Data::Struct(fields) => self.gen_struct(fields), - }; - tokens.extend(ts); - } -} - -impl Diagnostic { - fn gen_enum(&self, variants: Vec<&DiagnosticVariant>) -> TokenStream { - let ident = &self.ident; - let (impl_generics, ty_generics, where_clause) = &self.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); - - quote! { - impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { - #code_body - #help_body - #sev_body - } - } - } - - fn gen_struct(&self, fields: Fields<&DiagnosticField>) -> TokenStream { - let ident= &self.ident; - let (impl_generics, ty_generics, where_clause) = &self.generics.split_for_impl(); - let code_body = Code::gen_struct(self, &fields); - let help_body = Help::gen_struct(self, &fields); - let sev_body = Severity::gen_struct(self, &fields); - - quote! { - impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { - #code_body - #help_body - #sev_body - } - } - } -} diff --git a/miette-derive/src/severity.rs b/miette-derive/src/severity.rs index 754425e..4a67cbe 100644 --- a/miette-derive/src/severity.rs +++ b/miette-derive/src/severity.rs @@ -1,33 +1,47 @@ -use darling::{ast::Fields, error::Error as DarlingError, FromMeta}; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; -use syn::{Lit, LitStr, Meta, NestedMeta, Path}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream}, + Token, +}; -use crate::{Diagnostic, DiagnosticField, DiagnosticVariant}; +use crate::diagnostic::{Diagnostic, DiagnosticVariant}; -#[derive(Debug)] -pub struct Severity(pub Path); +pub struct Severity(pub syn::Path); -impl FromMeta for Severity { - fn from_string(arg: &str) -> Result { - Ok(Severity(LitStr::new(arg, Span::call_site()).parse()?)) - } - - fn from_list(items: &[NestedMeta]) -> Result { - match &items[0] { - NestedMeta::Meta(Meta::Path(p)) => Ok(Severity(p.clone())), - NestedMeta::Lit(Lit::Str(sev)) => Ok(Severity(sev.parse()?)), - _ => Err(DarlingError::custom( - "invalid severity format. Only literal names and string literals are accepted", - )), +impl Parse for Severity { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.parse::()?; + if ident == "severity" { + let la = input.lookahead1(); + if la.peek(syn::token::Paren) { + let content; + parenthesized!(content in input); + let la = content.lookahead1(); + if la.peek(syn::LitStr) { + let str = content.parse::()?; + Ok(Severity(str.parse()?)) + } else { + let path = content.parse::()?; + Ok(Severity(path)) + } + } else { + input.parse::()?; + Ok(Severity(input.parse::()?.parse()?)) + } + } else { + Err(syn::Error::new( + ident.span(), + "not a severity level.", + )) } } } - impl Severity { pub(crate) fn gen_enum( _diag: &Diagnostic, - variants: &[&DiagnosticVariant], + variants: &[DiagnosticVariant], ) -> Option { let sev_pairs = variants .iter() @@ -55,13 +69,11 @@ impl Severity { } } - pub(crate) fn gen_struct(diag: &Diagnostic, _fields: &Fields<&DiagnosticField>) -> Option { - diag.severity.as_ref().map(|sev| { - let sev = &sev.0; - quote! { - fn severity(&self) -> std::option::Option { - Some(miette::Severity::#sev) - } + pub(crate) fn gen_struct(&self) -> Option { + let sev = &self.0; + Some(quote! { + fn severity(&self) -> std::option::Option { + Some(miette::Severity::#sev) } }) } diff --git a/tests/derive.rs b/tests/derive.rs index 1a15c10..ef286e7 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -24,7 +24,6 @@ fn basic_struct() { #[test] fn basic_enum() { - #[derive(Debug, Diagnostic, Error)] #[error("welp")] enum Foo { @@ -35,14 +34,39 @@ fn basic_enum() { )] X, #[diagnostic(code = "foo::y")] - Y, + Y(usize), + #[diagnostic(code = "foo::z")] + Z { prop: String }, } assert_eq!("foo::x".to_string(), Foo::X.code().to_string()); - assert_eq!("foo::y".to_string(), Foo::Y.code().to_string()); + assert_eq!("foo::y".to_string(), Foo::Y(1).code().to_string()); + assert_eq!( + "foo::z".to_string(), + Foo::Z { prop: "bar".into() }.code().to_string() + ); assert_eq!(Some(Severity::Warning), Foo::X.severity()); - assert_eq!(None, Foo::Y.severity()); + assert_eq!(None, Foo::Y(1).severity()); +} + +#[test] +fn paren_code() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic(code("foo::bar::baz"))] + struct FooStruct; + + assert_eq!("foo::bar::baz".to_string(), FooStruct.code().to_string()); + + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + enum FooEnum { + #[diagnostic(code("foo::x"))] + X, + } + + assert_eq!("foo::x".to_string(), FooEnum::X.code().to_string()); } #[test] @@ -68,10 +92,7 @@ fn path_code() { fn path_severity() { #[derive(Debug, Diagnostic, Error)] #[error("welp")] - #[diagnostic( - code(foo::bar::baz), - severity(Warning) - )] + #[diagnostic(code(foo::bar::baz), severity(Warning))] struct FooStruct; assert_eq!(Some(Severity::Warning), FooStruct.severity()); @@ -79,10 +100,7 @@ fn path_severity() { #[derive(Debug, Diagnostic, Error)] #[error("welp")] enum FooEnum { - #[diagnostic( - code(foo::x), - severity(Warning), - )] + #[diagnostic(code(foo::x), severity(Warning))] X, } @@ -93,10 +111,7 @@ fn path_severity() { fn list_help() { #[derive(Debug, Diagnostic, Error)] #[error("welp")] - #[diagnostic( - code(foo::bar::baz), - help("try doing it better"), - )] + #[diagnostic(code(foo::bar::baz), help("try doing it better"))] struct FooStruct; assert_eq!( @@ -107,10 +122,7 @@ fn list_help() { #[derive(Debug, Diagnostic, Error)] #[error("welp")] enum FooEnum { - #[diagnostic( - code(foo::x), - help("try doing it better"), - )] + #[diagnostic(code(foo::x), help("try doing it better"))] X, } @@ -120,8 +132,6 @@ fn list_help() { ); } -// TODO: Darling doesn't support this, apparently: -// https://github.com/TedDriggs/darling/issues/145 /* #[test] fn fmt_help() { @@ -129,9 +139,9 @@ fn fmt_help() { #[error("welp")] #[diagnostic( code(foo::bar::baz), - help("{} {}", 1, "bar"), + help("{} {}", 1, self.0), )] - struct FooStruct; + struct FooStruct(String); assert_eq!( "1 bar".to_string(),