fix(derive): move to plain syn to fix darling issues

This commit is contained in:
Kat Marchán 2021-08-15 17:27:39 -07:00
parent 027c3b0a94
commit 9a78a94395
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
8 changed files with 376 additions and 225 deletions

View File

@ -14,4 +14,3 @@ proc-macro = true
proc-macro2 = "1.0"
quote = "1.0"
syn = "1.0.45"
darling = "0.13.0"

View File

@ -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<Self, DarlingError> {
Ok(Code(arg.into()))
}
fn from_list(items: &[NestedMeta]) -> Result<Self, DarlingError> {
match &items[0] {
NestedMeta::Meta(Meta::Path(p)) => Ok(Code(
p.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.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<Self> {
let ident = input.parse::<syn::Ident>()?;
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::<syn::LitStr>()?;
Ok(Code(str.value()))
} else {
let path = content.parse::<syn::Path>()?;
Ok(Code(
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::"),
))
}
} else {
input.parse::<Token![=]>()?;
Ok(Code(input.parse::<syn::LitStr>()?.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<TokenStream> {
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<TokenStream> {
let code = diag
.code
.as_ref()
.expect("`code` attribute is required for diagnostics.")
.to_string();
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
let code = &self.0;
Some(quote! {
fn code<'a>(&'a self) -> std::boxed::Box<dyn std::fmt::Display + 'a> {
std::boxed::Box::new(#code)

View File

@ -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<Severity>,
help: Option<Help>,
},
Enum {
ident: syn::Ident,
generics: syn::Generics,
variants: Vec<DiagnosticVariant>,
},
}
pub struct DiagnosticVariant {
pub ident: syn::Ident,
pub fields: syn::Fields,
pub code: Code,
pub severity: Option<Severity>,
pub help: Option<Help>,
}
impl Diagnostic {
pub fn from_derive_input(input: DeriveInput) -> Result<Self, syn::Error> {
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::<DiagnosticArg, Token![,]>::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::<DiagnosticArg, Token![,]>::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
}
}
}
}
}
}

View File

@ -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<Self> {
let ident = input.fork().parse::<syn::Ident>()?;
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"))
}
}
}

View File

@ -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<NestedMeta>,
pub args: Vec<syn::Expr>,
}
impl FromMeta for Help {
fn from_string(arg: &str) -> Result<Help, DarlingError> {
Ok(Help {
fmt: arg.into(),
args: Vec::new(),
})
}
fn from_list(items: &[NestedMeta]) -> Result<Help, DarlingError> {
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::<Result<Vec<_>, 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<Self> {
let ident = input.parse::<syn::Ident>()?;
if ident == "help" {
let la = input.lookahead1();
if la.peek(syn::token::Paren) {
let content;
parenthesized!(content in input);
let str = content.parse::<syn::LitStr>()?;
Ok(Help {
fmt: str.value(),
args: Vec::new(),
})
} else {
input.parse::<Token![=]>()?;
Ok(Help {
fmt: input.parse::<syn::LitStr>()?.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<TokenStream> {
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::<Vec<_>>();
@ -76,17 +83,12 @@ impl Help {
}
}
pub(crate) fn gen_struct(
diag: &Diagnostic,
_fields: &Fields<&DiagnosticField>,
) -> Option<TokenStream> {
diag.help.as_ref().map(|h| {
let fmt = &h.fmt;
let args = &h.args;
quote! {
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*)))
}
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
let fmt = &self.fmt;
let args = &self.args;
Some(quote! {
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*)))
}
})
}

View File

@ -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<DiagnosticVariant, DiagnosticField>,
generics: syn::Generics,
#[darling(default)]
code: Option<Code>,
#[darling(default)]
severity: Option<Severity>,
#[darling(default)]
help: Option<Help>,
}
#[derive(Debug, FromField)]
struct DiagnosticField {
ident: Option<syn::Ident>,
ty: syn::Type,
}
#[derive(Debug, FromVariant)]
#[darling(attributes(diagnostic))]
struct DiagnosticVariant {
ident: syn::Ident,
code: Code,
#[darling(default)]
severity: Option<Severity>,
#[darling(default)]
help: Option<Help>,
}
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
}
}
}
}

View File

@ -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<Self, DarlingError> {
Ok(Severity(LitStr::new(arg, Span::call_site()).parse()?))
}
fn from_list(items: &[NestedMeta]) -> Result<Self, DarlingError> {
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<Self> {
let ident = input.parse::<syn::Ident>()?;
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::<syn::LitStr>()?;
Ok(Severity(str.parse()?))
} else {
let path = content.parse::<syn::Path>()?;
Ok(Severity(path))
}
} else {
input.parse::<Token![=]>()?;
Ok(Severity(input.parse::<syn::LitStr>()?.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<TokenStream> {
let sev_pairs = variants
.iter()
@ -55,13 +69,11 @@ impl Severity {
}
}
pub(crate) fn gen_struct(diag: &Diagnostic, _fields: &Fields<&DiagnosticField>) -> Option<TokenStream> {
diag.severity.as_ref().map(|sev| {
let sev = &sev.0;
quote! {
fn severity(&self) -> std::option::Option<miette::Severity> {
Some(miette::Severity::#sev)
}
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
let sev = &self.0;
Some(quote! {
fn severity(&self) -> std::option::Option<miette::Severity> {
Some(miette::Severity::#sev)
}
})
}

View File

@ -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(),