diff --git a/examples/codegen.rs b/examples/codegen.rs index 8033021..abebb33 100644 --- a/examples/codegen.rs +++ b/examples/codegen.rs @@ -24,6 +24,20 @@ struct Alignable { #[derive(FormatArgs)] struct WithBounds<'a, T: std::fmt::Display + 'a>(&'a T); +#[allow(dead_code)] +#[derive(FormatArgs)] +struct WithAttributes { + #[format_args(rename = "renamed_field")] + field1: &'static str, + #[format_args(aliases = "alias")] + field2: &'static str, + #[format_args(ignore)] + ignored: &'static str +} + +#[derive(FormatArgs)] +struct TupleWithAttributes(#[format_args(rename = "field1")] i32); + fn main() { let mut prepared = PreparedFormat::prepare("{left}: {right}").unwrap(); prepared.newln(); @@ -49,4 +63,24 @@ fn main() { }); PreparedFormat::prepare("{}").unwrap().newln().print(&WithBounds(&256)); + + let mut prepared = PreparedFormat::prepare( +r#"WithAttributes {{ + renamed_field: {renamed_field} + field2: {field2} + alias: {alias} +}}"#).unwrap(); + prepared.newln(); + prepared.print(&WithAttributes { + field1: "field1", + field2: "field2", + ignored: "ignored" + }); + + match PreparedFormat::::prepare("{ignored}") { + Ok(_) => panic!("Field 'ignored' is not ignored"), + _ => { } + } + + PreparedFormat::prepare("TupleWithAttributes({field1})").unwrap().newln().print(&TupleWithAttributes(256)); } diff --git a/runtime-fmt-derive/src/ast.rs b/runtime-fmt-derive/src/ast.rs new file mode 100644 index 0000000..8c066bd --- /dev/null +++ b/runtime-fmt-derive/src/ast.rs @@ -0,0 +1,202 @@ + +use std::borrow::{Borrow, Cow}; +use std::collections::HashSet; +use syn; +use syn::Lit::Str; +use syn::MetaItem::{List, NameValue, Word}; +use syn::NestedMetaItem::MetaItem; +use ::context::Context; + +struct Attribute<'a, T> { + context: &'a Context, + name: &'static str, + value: Option +} + +impl<'a, T> Attribute<'a, T> { + + fn new(context: &'a Context, name: &'static str) -> Self { + Attribute { + context: context, + name: name, + value: None + } + } + + fn set(&mut self, value: T) { + if self.value.is_some() { + self.context.error(&format!("Duplicate attribute provided: {}", self.name)); + } + else { + self.value = Some(value); + } + } + + fn into(mut self) -> Option { + self.value.take() + } + +} + +struct BoolAttribute<'a> { + inner: Attribute<'a, ()> +} + +impl<'a> BoolAttribute<'a> { + + fn new(context: &'a Context, name: &'static str) -> Self { + BoolAttribute { + inner: Attribute::new(context, name) + } + } + + fn set(&mut self) { + self.inner.set(()); + } + + fn into(self) -> bool { + self.inner.into().is_some() + } + +} + +pub struct Container<'a> { + ast: &'a syn::DeriveInput, + + fields: Vec> +} + +impl<'a> Container<'a> { + + pub fn from_ast(ast: &'a syn::DeriveInput) -> Result { + let ctx = Context::new(); + + let variant = match ast.body { + syn::Body::Struct(ref variant) => variant, + _ => return Err("#[derive(FormatArgs)] is not implemented for enums".to_string()) + }; + + let fields = { + let mut fields = Vec::new(); + let mut field_names = HashSet::new(); + for (tuple_index, field) in variant.fields().iter().enumerate() { + if let Some(field) = Field::from_ast(&ctx, field, fields.len(), tuple_index) { + for alias in field.aliases() { + if !field_names.insert(*alias) { + ctx.error(&format!("Duplicate field alias: {}", alias)); + } + } + fields.push(field); + } + } + + fields + }; + + ctx.check().map(|_| { + Container { + ast: ast, + + fields: fields + } + }) + } + + pub fn ident(&self) -> &'a syn::Ident { + &self.ast.ident + } + + pub fn generics(&self) -> &'a syn::Generics { + &self.ast.generics + } + + pub fn fields(&self) -> &[Field<'a>] { + &self.fields + } + +} + +pub struct Field<'a> { + field_index: usize, + + ident: Cow<'a, syn::Ident>, + aliases: Vec<&'a str>, + + ty: &'a syn::Ty +} + +impl<'a> Field<'a> { + + pub fn from_ast(ctx: &Context, ast: &'a syn::Field, field_index: usize, tuple_index: usize) -> Option { + let ident = match ast.ident { + Some(ref ident) => Cow::Borrowed(ident), + None => Cow::Owned(syn::Ident::from(tuple_index)) + }; + + let mut aliases = Attribute::new(ctx, "aliases"); + let mut ignored = BoolAttribute::new(ctx, "ignore"); + let mut name = Attribute::new(ctx, "rename"); + + for attributes in ast.attrs.iter().filter_map(filter_format_attributes) { + for attribute in attributes { + match *attribute { + MetaItem(NameValue(ref ident, Str(ref value, _))) if ident == "aliases" => { + aliases.set(value.split(",").collect()); + } + MetaItem(NameValue(ref ident, Str(ref value, _))) if ident == "rename" => { + name.set(value.as_ref()); + } + MetaItem(Word(ref ident)) if ident == "ignore" => { + ignored.set(); + } + _ => ctx.error(&format!("Unrecognized attribute: {:?}", attribute)) + } + } + } + + let mut aliases = aliases.into().unwrap_or_else(Vec::new); + if let Some(name) = name.into().or(ast.ident.as_ref().map(|ident| ident.as_ref())) { + aliases.push(name); + } + + if !ignored.into() { + Some(Field { + field_index: field_index, + + ident: ident, + aliases: aliases, + + ty: &ast.ty + }) + } + else { + None + } + } + + pub fn index(&self) -> usize { + self.field_index + } + + pub fn ident(&self) -> &syn::Ident { + self.ident.borrow() + } + + pub fn aliases(&self) -> &[&'a str] { + &self.aliases + } + + pub fn ty(&self) -> &'a syn::Ty { + self.ty + } + +} + +fn filter_format_attributes(attr: &syn::Attribute) -> Option<&Vec> { + match attr.value { + List(ref name, ref items) if name == "format_args" => { + Some(items) + } + _ => None + } +} \ No newline at end of file diff --git a/runtime-fmt-derive/src/context.rs b/runtime-fmt-derive/src/context.rs new file mode 100644 index 0000000..9918da9 --- /dev/null +++ b/runtime-fmt-derive/src/context.rs @@ -0,0 +1,44 @@ + +use std::cell::RefCell; + +pub struct Context { + error: RefCell> +} + +impl Context { + + pub fn new() -> Self { + Context { + error: RefCell::new(Some(String::new())) + } + } + + pub fn error(&self, error: &str) { + let mut cur_error = self.error.borrow_mut(); + let mut cur_error = cur_error.get_or_insert_with(String::new); + + if !cur_error.is_empty() { + cur_error.push('\n'); + } + *cur_error += &error; + } + + pub fn check(&self) -> Result<(), String> { + self.error.borrow_mut() + .take().into_iter() + .filter(|s| !s.is_empty()) + .next() + .map_or(Ok(()), Err) + } + +} + +impl Drop for Context { + + fn drop(&mut self) { + if self.error.borrow().is_some() { + panic!("Failed to check for errors in context"); + } + } + +} \ No newline at end of file diff --git a/runtime-fmt-derive/src/lib.rs b/runtime-fmt-derive/src/lib.rs index 775a626..9e98f57 100644 --- a/runtime-fmt-derive/src/lib.rs +++ b/runtime-fmt-derive/src/lib.rs @@ -1,5 +1,6 @@ //! A custom-derive implementation for the `FormatArgs` trait. #![recursion_limit="128"] +#![feature(option_entry)] extern crate proc_macro; extern crate syn; @@ -7,63 +8,38 @@ extern crate syn; use proc_macro::TokenStream; +use ast::Container; + +mod ast; +mod context; + /// Derive a `FormatArgs` implementation for the provided input struct. -#[proc_macro_derive(FormatArgs)] +#[proc_macro_derive(FormatArgs, attributes(format_args))] pub fn derive_format_args(input: TokenStream) -> TokenStream { let string = input.to_string(); let ast = syn::parse_derive_input(&string).unwrap(); - implement(&ast).parse().unwrap() + match implement_format_trait(&ast) { + Ok(tokens) => tokens.parse().unwrap(), + Err(error) => panic!(error) + } } -fn implement(ast: &syn::DeriveInput) -> quote::Tokens { - // The rough structure of this (dummy_ident, extern crate/use) is based on - // how serde_derive does it. +fn implement_format_trait(ast: &syn::DeriveInput) -> Result { + let container = Container::from_ast(ast)?; - let ident = &ast.ident; - let variant = match ast.body { - syn::Body::Struct(ref variant) => variant, - _ => panic!("#[derive(FormatArgs)] is not implemented for enums") + let validate_name = build_validate_name(&container); + let validate_index = { + let max_index = container.fields().len(); + quote! { index < #max_index } }; + let get_child = build_get_child(&container); + let as_usize = build_as_usize(&container); + let ident = container.ident(); let dummy_ident = syn::Ident::new(format!("_IMPL_FORMAT_ARGS_FOR_{}", ident)); - - let (validate_name, validate_index, get_child, as_usize); - match *variant { - syn::VariantData::Struct(ref fields) => { - get_child = build_fields(fields); - as_usize = build_usize(ast, fields); - validate_index = quote! { false }; - - let index = 0..fields.len(); - let ident: Vec<_> = fields.iter() - .map(|field| field.ident.as_ref().unwrap()) - .map(ToString::to_string) - .collect(); - validate_name = quote! { - match name { - #(#ident => _Option::Some(#index),)* - _ => _Option::None, - } - }; - } - syn::VariantData::Tuple(ref fields) => { - get_child = build_fields(fields); - as_usize = build_usize(ast, fields); - validate_name = quote! { _Option::None }; - - let len = fields.len(); - validate_index = quote! { index < #len }; - } - syn::VariantData::Unit => { - validate_name = quote! { _Option::None }; - validate_index = quote! { false }; - get_child = quote! { panic!("bad index {}", index) }; - as_usize = get_child.clone(); - } - }; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); - quote! { + + Ok(quote! { #[allow(non_upper_case_globals, unused_attributes)] #[allow(unused_variables, unused_qualifications)] const #dummy_ident: () = { @@ -75,6 +51,7 @@ fn implement(ast: &syn::DeriveInput) -> quote::Tokens { fn validate_name(name: &str) -> _Option { #validate_name } + #[allow(unused_comparisons)] fn validate_index(index: usize) -> bool { #validate_index } @@ -88,36 +65,55 @@ fn implement(ast: &syn::DeriveInput) -> quote::Tokens { } } }; + }) +} + +fn build_validate_name<'a>(container: &Container<'a>) -> quote::Tokens { + let mut matches = quote::Tokens::new(); + for field in container.fields() { + let aliases = field.aliases(); + if !aliases.is_empty() { + let index = field.index(); + matches.append(quote! { #(#aliases)|* => _Option::Some(#index), }); + } + } + quote! { + match name { + #matches + _ => _Option::None + } } } -fn build_fields(fields: &[syn::Field]) -> quote::Tokens { - let index = 0..fields.len(); - let ty: Vec<_> = fields.iter().map(|field| &field.ty).collect(); - let ident: Vec<_> = fields.iter().enumerate().map(|(idx, field)| match field.ident { - Some(ref ident) => ident.clone(), - None => syn::Ident::from(idx), - }).collect(); +fn build_get_child<'a>(container: &Container<'a>) -> quote::Tokens { + let mut matches = quote::Tokens::new(); + for field in container.fields() { + let index = field.index(); + let ty = field.ty(); + let ident = field.ident(); + + matches.append(quote! { + #index => _runtime_fmt::codegen::combine::<__F, Self, #ty, _>( + |this| &this.#ident + ), + }); + } quote! { match index { - #( - #index => _runtime_fmt::codegen::combine::<__F, Self, #ty, _>( - |this| &this.#ident - ), - )* - _ => panic!("bad index {}", index) + #matches + _ => panic!("Bad index: {}", index) } } } -fn build_usize(ast: &syn::DeriveInput, fields: &[syn::Field]) -> quote::Tokens { - let self_ = &ast.ident; - let (_, ty_generics, where_clause) = ast.generics.split_for_impl(); +fn build_as_usize<'a>(container: &'a Container) -> quote::Tokens { + let self_ = container.ident(); + let (_, ty_generics, where_clause) = container.generics().split_for_impl(); // To avoid causing trouble with lifetime elision rules, an explicit // lifetime for the input and output is used. let lifetime = syn::Ident::new("'__as_usize_inner"); - let mut generics2 = ast.generics.clone(); + let mut generics2 = container.generics().clone(); generics2.lifetimes.insert(0, syn::LifetimeDef { attrs: vec![], lifetime: syn::Lifetime { ident: lifetime.clone() }, @@ -125,15 +121,14 @@ fn build_usize(ast: &syn::DeriveInput, fields: &[syn::Field]) -> quote::Tokens { }); let (impl_generics, _, _) = generics2.split_for_impl(); - let mut result = quote::Tokens::new(); - for (idx, field) in fields.iter().enumerate() { - let ident = match field.ident { - Some(ref ident) => ident.clone(), - None => syn::Ident::from(idx), - }; - let ty = &field.ty; - result.append(quote! { - #idx => { + let mut matches = quote::Tokens::new(); + for field in container.fields() { + let index = field.index(); + let ty = field.ty(); + let ident = field.ident(); + + matches.append(quote! { + #index => { fn inner #impl_generics (this: &#lifetime #self_ #ty_generics) -> &#lifetime #ty #where_clause { &this.#ident } @@ -144,8 +139,8 @@ fn build_usize(ast: &syn::DeriveInput, fields: &[syn::Field]) -> quote::Tokens { quote! { match index { - #result + #matches _ => panic!("bad index {}", index) } } -} +} \ No newline at end of file diff --git a/tests/codegen.rs b/tests/codegen.rs new file mode 100644 index 0000000..ca08f67 --- /dev/null +++ b/tests/codegen.rs @@ -0,0 +1,106 @@ +#[macro_use] extern crate runtime_fmt_derive; +extern crate runtime_fmt; + +use runtime_fmt::{FormatArgs, PreparedFormat}; + +#[test] +#[allow(dead_code)] +fn test_struct() { + #[derive(FormatArgs)] + struct Struct { + #[format_args(aliases = "alias1,alias2")] + field1: &'static str, + field2: &'static str, + field3: usize, + #[format_args(rename = "renamed")] + field4: &'static str, + #[format_args(ignore)] + ignored: &'static str + } + + assert_eq!(Struct::validate_name("field1").is_some(), true); + assert_eq!(Struct::validate_name("field1"), Struct::validate_name("alias1")); + assert_eq!(Struct::validate_name("field1"), Struct::validate_name("alias2")); + + assert_eq!(Struct::validate_name("field2").is_some(), true); + assert_eq!(Struct::validate_name("field3").is_some(), true); + + assert_eq!(Struct::validate_name("field4").is_some(), false); + assert_eq!(Struct::validate_name("renamed").is_some(), true); + + assert_eq!(Struct::validate_name("ignored").is_some(), false); + + assert_eq!(Struct::validate_index(0), true); + assert_eq!(Struct::validate_index(1), true); + assert_eq!(Struct::validate_index(2), true); + assert_eq!(Struct::validate_index(3), true); + assert_eq!(Struct::validate_index(4), false); + + assert_eq!(Struct::as_usize(0).is_some(), false); + assert_eq!(Struct::as_usize(1).is_some(), false); + assert_eq!(Struct::as_usize(2).is_some(), true); + assert_eq!(Struct::as_usize(3).is_some(), false); + + let value = Struct { + field1: "value1", + field2: "value2", + field3: 123456, + field4: "value4", + ignored: "ignored" + }; + + let fmt = PreparedFormat::prepare("{field1} {alias1} {alias2} {field2} {field3} {renamed}").unwrap().format(&value); + assert_eq!(fmt, "value1 value1 value1 value2 123456 value4"); + + let fmt = PreparedFormat::prepare("{0} {1} {2} {3}").unwrap().format(&value); + assert_eq!(fmt, "value1 value2 123456 value4"); +} + +#[test] +fn test_tuple() { + #[derive(FormatArgs)] + struct Tuple( + #[format_args(aliases = "alias1,alias2")] + &'static str, + &'static str, + usize, + #[format_args(rename = "renamed")] + &'static str, + #[format_args(ignore)] + &'static str + ); + + assert_eq!(Tuple::validate_name("alias1").is_some(), true); + assert_eq!(Tuple::validate_name("alias1"), Tuple::validate_name("alias2")); + assert_eq!(Tuple::validate_name("renamed").is_some(), true); + + assert_eq!(Tuple::validate_index(0), true); + assert_eq!(Tuple::validate_index(1), true); + assert_eq!(Tuple::validate_index(2), true); + assert_eq!(Tuple::validate_index(3), true); + assert_eq!(Tuple::validate_index(4), false); + + assert_eq!(Tuple::as_usize(0).is_some(), false); + assert_eq!(Tuple::as_usize(1).is_some(), false); + assert_eq!(Tuple::as_usize(2).is_some(), true); + assert_eq!(Tuple::as_usize(3).is_some(), false); + + let value = Tuple("value1", "value2", 123456, "value3", "ignored"); + + let fmt = PreparedFormat::prepare("{alias1} {alias2} {renamed}").unwrap().format(&value); + assert_eq!(fmt, "value1 value1 value3"); + + let fmt = PreparedFormat::prepare("{0} {1} {2} {3}").unwrap().format(&value); + assert_eq!(fmt, "value1 value2 123456 value3"); +} + +#[test] +fn test_unit() { + #[derive(FormatArgs)] + struct Unit; + + assert_eq!(Unit::validate_index(0), false); + + let fmt = PreparedFormat::prepare("").unwrap().format(&Unit); + assert_eq!(fmt, ""); +} \ No newline at end of file