New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Automatic Generic-Derived Qualifiers #145
Changes from all commits
8d2c7d4
fedb020
64b2a0d
f9ca456
28751e0
268352c
5de9589
377de48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,12 @@ | ||
use crate::ast::{Enum, Field, Input, Struct}; | ||
use crate::fmt::DisplayFormatMarking; | ||
use proc_macro2::TokenStream; | ||
use quote::{format_ident, quote, quote_spanned, ToTokens}; | ||
use syn::spanned::Spanned; | ||
use syn::{Data, DeriveInput, GenericArgument, Member, PathArguments, Result, Type, Visibility}; | ||
use syn::{ | ||
parse_quote, Data, DeriveInput, GenericArgument, Member, PathArguments, Result, Type, | ||
Visibility, | ||
}; | ||
|
||
pub fn derive(node: &DeriveInput) -> Result<TokenStream> { | ||
let input = Input::from_syn(node)?; | ||
|
@@ -114,6 +118,48 @@ fn impl_struct(input: Struct) -> TokenStream { | |
None | ||
}; | ||
let display_impl = display_body.map(|body| { | ||
let mut extra_predicates = Vec::new(); | ||
for field in input | ||
.attrs | ||
.display | ||
.iter() | ||
.flat_map(|d| d.iter_fmt_types(input.fields.as_slice())) | ||
{ | ||
let (ty, bound, ast): ( | ||
&syn::Type, | ||
syn::punctuated::Punctuated<syn::TypeParamBound, _>, | ||
&syn::Field, | ||
); | ||
match field { | ||
DisplayFormatMarking::Debug(f) => { | ||
ty = f.ty; | ||
bound = parse_quote! { ::std::fmt::Debug }; | ||
ast = f.original; | ||
} | ||
DisplayFormatMarking::Display(f) => { | ||
ty = f.ty; | ||
bound = parse_quote! { ::std::fmt::Display }; | ||
ast = f.original; | ||
} | ||
} | ||
// If a generic is at all present, a constraint will be applied to the field type | ||
// This may create redundant `AlwaysDebug<T>: Debug` scenarios, but covers T: Debug and &T: Debug cleanly | ||
let mut usages = GenericUsageVisitor::new_unmarked( | ||
input.generics.type_params().map(|p| p.ident.clone()), | ||
); | ||
syn::visit::visit_field(&mut usages, ast); | ||
if usages.iter_marked().next().is_some() { | ||
extra_predicates.push(syn::WherePredicate::Type(syn::PredicateType { | ||
bounded_ty: ty.clone(), | ||
colon_token: syn::token::Colon::default(), | ||
bounds: bound, | ||
lifetimes: None, | ||
})); | ||
} | ||
} | ||
|
||
let where_clause = augment_where_clause(where_clause, extra_predicates); | ||
|
||
quote! { | ||
#[allow(unused_qualifications)] | ||
impl #impl_generics std::fmt::Display for #ty #ty_generics #where_clause { | ||
|
@@ -144,8 +190,46 @@ fn impl_struct(input: Struct) -> TokenStream { | |
} | ||
}); | ||
|
||
let error_trait = spanned_error_trait(input.original); | ||
let (generic_field_types, generics_in_from_types): (Vec<&syn::Type>, Vec<proc_macro2::Ident>) = { | ||
let mut generics_in_from_types = GenericUsageVisitor::new_unmarked( | ||
input.generics.type_params().map(|p| p.ident.clone()), | ||
); | ||
let mut generic_field_types = Vec::new(); | ||
if let Some(from_field) = input.from_field() { | ||
let mut generics_in_this_field = GenericUsageVisitor::new_unmarked( | ||
input.generics.type_params().map(|p| p.ident.clone()), | ||
); | ||
syn::visit::visit_type(&mut generics_in_this_field, &from_field.original.ty); | ||
if generics_in_from_types.mark_from(generics_in_this_field.iter_marked()) { | ||
generic_field_types.push(from_field.ty); | ||
} | ||
} | ||
( | ||
generic_field_types, | ||
generics_in_from_types | ||
.generics | ||
.into_iter() | ||
.map(|(k, _v)| k) | ||
.collect(), | ||
) | ||
}; | ||
|
||
let extra_predicates = | ||
std::iter::once(parse_quote! { Self: ::std::fmt::Display + ::std::fmt::Debug }) | ||
.chain( | ||
generics_in_from_types | ||
.into_iter() | ||
.map(|generic| parse_quote! { #generic: 'static }), | ||
) | ||
.chain( | ||
generic_field_types | ||
.into_iter() | ||
.map(|ty| parse_quote! { #ty: ::std::error::Error }), | ||
); | ||
|
||
let where_clause = augment_where_clause(where_clause, extra_predicates); | ||
|
||
let error_trait = spanned_error_trait(input.original); | ||
quote! { | ||
#[allow(unused_qualifications)] | ||
impl #impl_generics #error_trait for #ty #ty_generics #where_clause { | ||
|
@@ -308,6 +392,47 @@ fn impl_enum(input: Enum) -> TokenStream { | |
} else { | ||
None | ||
}; | ||
|
||
let mut extra_predicates = Vec::new(); | ||
for field in input.variants.iter().flat_map(|v| { | ||
v.attrs | ||
.display | ||
.iter() | ||
.flat_map(move |d| d.iter_fmt_types(&v.fields)) | ||
}) { | ||
let (ty, bound, ast): ( | ||
&syn::Type, | ||
syn::punctuated::Punctuated<syn::TypeParamBound, _>, | ||
&syn::Field, | ||
); | ||
match field { | ||
DisplayFormatMarking::Debug(f) => { | ||
ty = f.ty; | ||
bound = parse_quote! { ::std::fmt::Debug }; | ||
ast = f.original; | ||
} | ||
DisplayFormatMarking::Display(f) => { | ||
ty = f.ty; | ||
bound = parse_quote! { ::std::fmt::Display }; | ||
ast = f.original; | ||
} | ||
} | ||
// If a generic is at all present, a constraint will be applied to the field type | ||
// This may create redundant `AlwaysDebug<T>: Debug` scenarios, but covers T: Debug and &T: Debug cleanly | ||
let mut usages = GenericUsageVisitor::new_unmarked( | ||
input.generics.type_params().map(|p| p.ident.clone()), | ||
); | ||
syn::visit::visit_field(&mut usages, ast); | ||
if usages.iter_marked().next().is_some() { | ||
extra_predicates.push(syn::WherePredicate::Type(syn::PredicateType { | ||
bounded_ty: ty.clone(), | ||
colon_token: syn::token::Colon::default(), | ||
bounds: bound, | ||
lifetimes: None, | ||
})); | ||
} | ||
} | ||
|
||
let arms = input.variants.iter().map(|variant| { | ||
let display = match &variant.attrs.display { | ||
Some(display) => display.to_token_stream(), | ||
|
@@ -325,6 +450,9 @@ fn impl_enum(input: Enum) -> TokenStream { | |
#ty::#ident #pat => #display | ||
} | ||
}); | ||
|
||
let where_clause = augment_where_clause(where_clause, extra_predicates); | ||
|
||
Some(quote! { | ||
#[allow(unused_qualifications)] | ||
impl #impl_generics std::fmt::Display for #ty #ty_generics #where_clause { | ||
|
@@ -364,8 +492,50 @@ fn impl_enum(input: Enum) -> TokenStream { | |
}) | ||
}); | ||
|
||
let error_trait = spanned_error_trait(input.original); | ||
let (generic_field_types, generics_in_from_types): (Vec<&syn::Type>, Vec<proc_macro2::Ident>) = { | ||
let mut generics_in_from_types = GenericUsageVisitor::new_unmarked( | ||
input.generics.type_params().map(|p| p.ident.clone()), | ||
); | ||
let mut generic_field_types = Vec::new(); | ||
for from_field in input | ||
.variants | ||
.iter() | ||
.filter_map(|variant| variant.from_field()) | ||
{ | ||
let mut generics_in_this_field = GenericUsageVisitor::new_unmarked( | ||
input.generics.type_params().map(|p| p.ident.clone()), | ||
); | ||
syn::visit::visit_type(&mut generics_in_this_field, &from_field.original.ty); | ||
if generics_in_from_types.mark_from(generics_in_this_field.iter_marked()) { | ||
generic_field_types.push(from_field.ty); | ||
} | ||
} | ||
( | ||
generic_field_types, | ||
generics_in_from_types | ||
.generics | ||
.into_iter() | ||
.map(|(k, _v)| k) | ||
.collect(), | ||
) | ||
}; | ||
|
||
let extra_predicates = | ||
std::iter::once(parse_quote! { Self: ::std::fmt::Display + ::std::fmt::Debug }) | ||
.chain( | ||
generics_in_from_types | ||
.into_iter() | ||
.map(|generic| parse_quote! { #generic: 'static }), | ||
) | ||
.chain( | ||
generic_field_types | ||
.into_iter() | ||
.map(|ty| parse_quote! { #ty: ::std::error::Error }), | ||
); | ||
|
||
let where_clause = augment_where_clause(where_clause, extra_predicates); | ||
|
||
let error_trait = spanned_error_trait(input.original); | ||
quote! { | ||
#[allow(unused_qualifications)] | ||
impl #impl_generics #error_trait for #ty #ty_generics #where_clause { | ||
|
@@ -377,6 +547,132 @@ fn impl_enum(input: Enum) -> TokenStream { | |
} | ||
} | ||
|
||
#[cfg_attr(test, derive(Debug))] | ||
#[derive(Clone)] | ||
struct GenericUsageVisitor { | ||
generics: std::collections::HashMap<proc_macro2::Ident, bool>, | ||
} | ||
|
||
impl GenericUsageVisitor { | ||
pub fn new<TPairs>(generics: TPairs) -> Self | ||
where | ||
TPairs: IntoIterator<Item = (syn::Ident, bool)>, | ||
{ | ||
Self { | ||
generics: generics.into_iter().collect(), | ||
} | ||
} | ||
|
||
pub fn new_unmarked<TIdents>(generics: TIdents) -> Self | ||
where | ||
TIdents: IntoIterator<Item = syn::Ident>, | ||
{ | ||
Self::new(generics.into_iter().map(|ident| (ident, false))) | ||
} | ||
|
||
pub fn iter_marked(&self) -> impl Iterator<Item = &proc_macro2::Ident> { | ||
self.generics.iter().filter(|(_k, v)| **v).map(|(k, _v)| k) | ||
} | ||
|
||
#[allow(dead_code)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No dead code please. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad- that one got left behind- I must've forgotten to tag it |
||
pub fn is_marked<'a, T: PartialEq<&'a proc_macro2::Ident> + 'a>(&'a self, item: T) -> bool { | ||
self.iter_marked().any(|ident| item == ident) | ||
} | ||
|
||
pub fn mark_from<'a, TMarkedSource: IntoIterator<Item = &'a proc_macro2::Ident> + 'a>( | ||
&'a mut self, | ||
Comment on lines
+582
to
+583
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The - pub fn mark_from<'a, TMarkedSource: IntoIterator<Item = &'a proc_macro2::Ident> + 'a>(
- &'a mut self,
+ pub fn mark_from<'a, TMarkedSource: IntoIterator<Item = &'a proc_macro2::Ident>>(
+ &mut self, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hum. I thought if I had one explicit (the one used in |
||
source: TMarkedSource, | ||
) -> bool { | ||
let mut any_marked = false; | ||
for other_marked in source { | ||
if let Some(is_marked) = self.generics.get_mut(other_marked) { | ||
*is_marked = true; | ||
any_marked = true; | ||
} | ||
} | ||
any_marked | ||
} | ||
} | ||
|
||
impl<'ast> syn::visit::Visit<'ast> for GenericUsageVisitor { | ||
fn visit_type_path(&mut self, i: &'ast syn::TypePath) { | ||
if let Some(ident) = i.path.get_ident() { | ||
if let Some(entry) = self.generics.get_mut(ident) { | ||
*entry = true; | ||
} | ||
} | ||
syn::visit::visit_type_path(self, i); | ||
} | ||
|
||
fn visit_type_param(&mut self, i: &'ast syn::TypeParam) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not reachable — except maybe in contrived code like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used this to deep-search for generic usages because- when a generic wasn't used- I was running into circumstances where fixed types would succeed in compilation where they should not, because it generated impossible/unlikely bounds, such as |
||
if let Some(entry) = self.generics.get_mut(&i.ident) { | ||
*entry = true; | ||
} | ||
syn::visit::visit_type_param(self, i); | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test_visitors { | ||
use proc_macro2::Span; | ||
|
||
use crate::expand::GenericUsageVisitor; | ||
|
||
#[test] | ||
fn test_generic_usage_visitor() { | ||
fn ident_for<'a>(ident_str: &'a str) -> syn::Ident { | ||
syn::Ident::new(ident_str, Span::call_site()) | ||
} | ||
|
||
let field: syn::Variant = syn::parse_quote! { X(Foo<Bar>) }; | ||
|
||
let mut visitor = GenericUsageVisitor::new( | ||
vec!["Bar", "Baz"] | ||
.into_iter() | ||
.map(|ident_str| (ident_for(ident_str), false)), | ||
); | ||
syn::visit::visit_variant(&mut visitor, &field); | ||
assert_eq!( | ||
visitor.generics.get(&ident_for("Bar")), | ||
Some(&true), | ||
"Bar must be marked" | ||
); | ||
assert_eq!( | ||
visitor.generics.get(&ident_for("Baz")), | ||
Some(&false), | ||
"Baz must be present but not marked" | ||
); | ||
} | ||
} | ||
|
||
fn augment_where_clause<TPredicates>( | ||
where_clause: Option<&syn::WhereClause>, | ||
extra_predicates: TPredicates, | ||
) -> Option<syn::WhereClause> | ||
where | ||
TPredicates: IntoIterator<Item = syn::WherePredicate>, | ||
{ | ||
let mut extra_predicates = extra_predicates.into_iter().peekable(); | ||
if extra_predicates.peek().is_none() { | ||
return where_clause.cloned(); | ||
} | ||
Some(match where_clause { | ||
Some(w) => syn::WhereClause { | ||
where_token: w.where_token, | ||
predicates: w | ||
.predicates | ||
.iter() | ||
.cloned() | ||
.chain(extra_predicates) | ||
.collect(), | ||
}, | ||
None => syn::WhereClause { | ||
where_token: syn::token::Where::default(), | ||
predicates: extra_predicates.into_iter().collect(), | ||
}, | ||
}) | ||
} | ||
|
||
fn fields_pat(fields: &[Field]) -> TokenStream { | ||
let mut members = fields.iter().map(|field| &field.member).peekable(); | ||
match members.peek() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general these changes in expand.rs are way more code than I would expect to need to add for this feature. Please see #148 for the approach I used instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your
Field.contains_generic
approach combined with thescope
abstraction seem to be the biggest contributors to code reduction, andInferredBounds
is doing some serious work considering its length.I'm curious, however, if there's some way to describe the "intent" for each bound as it's read, allowing a single
InferredBounds
-esque type to track the various usage forms to generalize the solution beyonderror
anddisplay
bounds.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is how https://github.com/dtolnay/reflect works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll keep an eye out for opportunities to use that library- but I suspect it would be a bit much in the way of overhead for this task? Then again, being a proc macro, its dependencies (hopefully?) aren't compiled into the binary it helps to produce unless explicitly requested.