Expand custom derives
diff --git a/.travis.yml b/.travis.yml
index 061ab83..73ac924 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,14 +20,14 @@
build)
cargo build --no-default-features &&
cargo build &&
- cargo build --features full &&
- cargo doc --features full
+ cargo build --features 'full expand' &&
+ cargo doc --features 'full expand'
;;
test)
- cargo test --features 'full aster visit'
+ cargo test --features 'full aster expand visit'
;;
clippy)
- cargo build --features 'full aster visit clippy'
+ cargo build --features 'full aster expand visit clippy'
;;
esac
diff --git a/Cargo.toml b/Cargo.toml
index 9fe2f0b..f22d2a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@
[features]
default = ["parsing", "printing"]
aster = []
+expand = ["full", "parsing", "printing"]
full = []
parsing = ["unicode-xid"]
printing = ["quote"]
@@ -24,5 +25,6 @@
[dev-dependencies]
syntex_pos = "0.45.0"
syntex_syntax = "0.45.0"
+tempdir = "0.3.5"
time = "0.1.35"
walkdir = "0.1.8"
diff --git a/src/item.rs b/src/item.rs
index be58fbb..cc5b7c3 100644
--- a/src/item.rs
+++ b/src/item.rs
@@ -80,6 +80,20 @@
Mac(Mac),
}
+impl From<MacroInput> for Item {
+ fn from(input: MacroInput) -> Item {
+ Item {
+ ident: input.ident,
+ vis: input.vis,
+ attrs: input.attrs,
+ node: match input.body {
+ Body::Enum(variants) => ItemKind::Enum(variants, input.generics),
+ Body::Struct(variant_data) => ItemKind::Struct(variant_data, input.generics),
+ },
+ }
+ }
+}
+
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ViewPath {
/// `foo::bar::baz as quux`
@@ -270,6 +284,8 @@
item_mac
));
+ named!(pub items -> Vec<Item>, many0!(item));
+
named!(item_mac -> Item, do_parse!(
attrs: many0!(outer_attr) >>
path: ident >>
@@ -489,7 +505,7 @@
items: alt!(
punct!(";") => { |_| None }
|
- delimited!(punct!("{"), many0!(item), punct!("}")) => { Some }
+ delimited!(punct!("{"), items, punct!("}")) => { Some }
) >>
(Item {
ident: id,
diff --git a/src/krate.rs b/src/krate.rs
index 4827088..e8ded24 100644
--- a/src/krate.rs
+++ b/src/krate.rs
@@ -11,11 +11,11 @@
pub mod parsing {
use super::*;
use attr::parsing::inner_attr;
- use item::parsing::item;
+ use item::parsing::items;
named!(pub krate -> Crate, do_parse!(
attrs: many0!(inner_attr) >>
- items: many0!(item) >>
+ items: items >>
(Crate {
shebang: None,
attrs: attrs,
diff --git a/src/lib.rs b/src/lib.rs
index 603e094..fd85720 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@
#![cfg_attr(feature = "clippy", plugin(clippy))]
#[cfg(feature = "printing")]
+#[macro_use]
extern crate quote;
#[cfg(feature = "parsing")]
@@ -68,6 +69,11 @@
mod op;
pub use op::{BinOp, UnOp};
+#[cfg(feature = "expand")]
+mod registry;
+#[cfg(feature = "expand")]
+pub use registry::{CustomDerive, Expanded, Registry};
+
#[cfg(feature = "parsing")]
mod space;
@@ -109,6 +115,11 @@
}
#[cfg(feature = "full")]
+ pub fn parse_items(input: &str) -> Result<Vec<Item>, String> {
+ unwrap("items", item::parsing::items, input)
+ }
+
+ #[cfg(feature = "full")]
pub fn parse_expr(input: &str) -> Result<Expr, String> {
unwrap("expression", expr::parsing::expr, input)
}
diff --git a/src/registry.rs b/src/registry.rs
new file mode 100644
index 0000000..8a6ce7b
--- /dev/null
+++ b/src/registry.rs
@@ -0,0 +1,335 @@
+use super::{Attribute, AttrStyle, Body, Crate, Ident, Item, ItemKind, MacroInput, MetaItem};
+
+use std::collections::BTreeMap as Map;
+use std::fs::File;
+use std::io::{Read, Write};
+use std::path::Path;
+
+/// Implementation of a custom derive. Custom derives take a struct or enum and
+/// expand it into zero or more items, typically `impl` items.
+pub trait CustomDerive {
+ /// Expand the given struct or enum. If this custom derive modifies the
+ /// input item or preserves it unmodified, it must be returned back in the
+ /// `original` field of Expanded. The custom derive may discard the input
+ /// item by setting `original` to None.
+ fn expand(&self, input: MacroInput) -> Result<Expanded, String>;
+}
+
+/// Produced by expanding a custom derive.
+pub struct Expanded {
+ /// The items (typically `impl` items) constructed by the custom derive.
+ pub new_items: Vec<Item>,
+ /// The input to the custom derive, whether modified or unmodified. If the
+ /// custom derive discards the input item it may do so by setting `original`
+ /// to None.
+ pub original: Option<MacroInput>,
+}
+
+/// Registry of custom derives. Callers add custom derives to a registry, then
+/// use the registry to expand those derives in a source file.
+#[derive(Default)]
+pub struct Registry<'a> {
+ derives: Map<String, Box<CustomDerive + 'a>>,
+}
+
+impl<T> CustomDerive for T
+ where T: Fn(MacroInput) -> Result<Expanded, String>
+{
+ fn expand(&self, input: MacroInput) -> Result<Expanded, String> {
+ self(input)
+ }
+}
+
+impl<'a> Registry<'a> {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Register a custom derive. A `fn(MacroInput) -> Result<Expanded, String>`
+ /// may be used as a custom derive.
+ ///
+ /// ```ignore
+ /// registry.add_derive("Serialize", expand_serialize);
+ /// ```
+ pub fn add_derive<T>(&mut self, name: &str, derive: T)
+ where T: CustomDerive + 'a
+ {
+ self.derives.insert(name.into(), Box::new(derive));
+ }
+
+ /// Read Rust source code from the `src` file, expand the custom derives
+ /// that have been registered, and write the result to the `dst` file.
+ pub fn expand_file<S, D>(&self, src: S, dst: D) -> Result<(), String>
+ where S: AsRef<Path>,
+ D: AsRef<Path>
+ {
+ // Open the src file
+ let mut src = match File::open(src) {
+ Ok(open) => open,
+ Err(err) => return Err(err.to_string()),
+ };
+
+ // Read the contents of the src file to a String
+ let mut content = String::new();
+ if let Err(err) = src.read_to_string(&mut content) {
+ return Err(err.to_string());
+ }
+
+ // Parse the contents
+ let krate = try!(super::parse_crate(&content));
+
+ // Expand
+ let expanded = try!(expand_crate(self, krate));
+
+ // Print the expanded code to a String
+ let out = quote!(#expanded).to_string();
+
+ // Create or truncate the dst file, opening in write-only mode
+ let mut dst = match File::create(dst) {
+ Ok(create) => create,
+ Err(err) => return Err(err.to_string()),
+ };
+
+ // Write expanded code to the dst file
+ if let Err(err) = dst.write_all(out.as_bytes()) {
+ return Err(err.to_string());
+ }
+
+ Ok(())
+ }
+}
+
+fn expand_crate(reg: &Registry, krate: Crate) -> Result<Crate, String> {
+ let mut items = Vec::new();
+ for item in krate.items {
+ try!(expand_item(reg, item, Vec::new(), &mut items));
+ }
+ Ok(Crate { items: items, ..krate })
+}
+
+fn expand_item(reg: &Registry, mut item: Item, cfg: Vec<MetaItem>, out: &mut Vec<Item>) -> Result<(), String> {
+ let (body, generics) = match item.node {
+ ItemKind::Enum(variants, generics) => (Body::Enum(variants), generics),
+ ItemKind::Struct(variant_data, generics) => (Body::Struct(variant_data), generics),
+ _ => {
+ // Custom derives cannot apply to this item, preserve it unmodified
+ item.attrs.extend(combine_cfgs(cfg));
+ out.push(item);
+ return Ok(());
+ }
+ };
+ let macro_input = MacroInput {
+ ident: item.ident,
+ vis: item.vis,
+ attrs: item.attrs,
+ generics: generics,
+ body: body,
+ };
+ expand_macro_input(reg, macro_input, cfg, out)
+}
+
+fn expand_macro_input(reg: &Registry,
+ mut input: MacroInput,
+ inherited_cfg: Vec<MetaItem>,
+ out: &mut Vec<Item>)
+ -> Result<(), String> {
+ let mut derives = Vec::new();
+ let mut all_cfg = inherited_cfg;
+
+ // Find custom derives on this item, removing them from the input
+ input.attrs = input.attrs
+ .into_iter()
+ .flat_map(|attr| {
+ let (new_derives, cfg, attr) = parse_attr(reg, attr);
+ derives.extend(new_derives);
+ all_cfg.extend(cfg);
+ attr
+ })
+ .collect();
+
+ // Expand each custom derive
+ for derive in derives {
+ let expanded = try!(reg.derives[derive.name.as_ref()].expand(input));
+
+ for new_item in expanded.new_items {
+ let mut extended_cfg = all_cfg.clone();
+ extended_cfg.extend(derive.cfg.clone());
+ try!(expand_item(reg, new_item, extended_cfg, out));
+ }
+
+ input = match expanded.original {
+ Some(input) => input,
+ None => return Ok(()),
+ };
+ }
+
+ input.attrs.extend(combine_cfgs(all_cfg));
+ out.push(input.into());
+ Ok(())
+}
+
+struct Derive {
+ name: Ident,
+ /// If the custom derive was behind a cfg_attr
+ cfg: Option<MetaItem>,
+}
+
+/// Pull custom derives and cfgs out of the given Attribute.
+fn parse_attr(reg: &Registry, attr: Attribute) -> (Vec<Derive>, Vec<MetaItem>, Option<Attribute>) {
+ if attr.style != AttrStyle::Outer || attr.is_sugared_doc {
+ return (Vec::new(), Vec::new(), Some(attr));
+ }
+
+ let (name, nested) = match attr.value {
+ MetaItem::List(name, nested) => (name, nested),
+ _ => return (Vec::new(), Vec::new(), Some(attr)),
+ };
+
+ match name.as_ref() {
+ "derive" => {
+ let (derives, attr) = parse_derive_attr(reg, nested);
+ let derives = derives.into_iter()
+ .map(|d| {
+ Derive {
+ name: d,
+ cfg: None,
+ }
+ })
+ .collect();
+ (derives, Vec::new(), attr)
+ }
+ "cfg_attr" => {
+ let (derives, attr) = parse_cfg_attr(reg, nested);
+ (derives, Vec::new(), attr)
+ }
+ "cfg" => (Vec::new(), nested, None),
+ _ => {
+ // Rebuild the original attribute because it was destructured above
+ let attr = Attribute {
+ style: AttrStyle::Outer,
+ value: MetaItem::List(name, nested),
+ is_sugared_doc: false,
+ };
+ (Vec::new(), Vec::new(), Some(attr))
+ }
+ }
+}
+
+/// Assuming the given nested meta-items came from a #[derive(...)] attribute,
+/// pull out the ones that are custom derives.
+fn parse_derive_attr(reg: &Registry, nested: Vec<MetaItem>) -> (Vec<Ident>, Option<Attribute>) {
+ let mut derives = Vec::new();
+
+ let remaining: Vec<_> = nested.into_iter()
+ .flat_map(|meta| {
+ let word = match meta {
+ MetaItem::Word(word) => word,
+ _ => return Some(meta),
+ };
+ if reg.derives.contains_key(word.as_ref()) {
+ derives.push(word);
+ None
+ } else {
+ Some(MetaItem::Word(word))
+ }
+ })
+ .collect();
+
+ let attr = if remaining.is_empty() {
+ // Elide an empty #[derive()]
+ None
+ } else {
+ Some(Attribute {
+ style: AttrStyle::Outer,
+ value: MetaItem::List("derive".into(), remaining),
+ is_sugared_doc: false,
+ })
+ };
+
+ (derives, attr)
+}
+
+/// Assuming the given nested meta-items came from a #[cfg_attr(...)] attribute,
+/// pull out any custom derives contained within.
+fn parse_cfg_attr(reg: &Registry, nested: Vec<MetaItem>) -> (Vec<Derive>, Option<Attribute>) {
+ if nested.len() != 2 {
+ let attr = Attribute {
+ style: AttrStyle::Outer,
+ value: MetaItem::List("cfg_attr".into(), nested),
+ is_sugared_doc: false,
+ };
+ return (Vec::new(), Some(attr));
+ }
+
+ let mut iter = nested.into_iter();
+ let cfg = iter.next().unwrap();
+ let arg = iter.next().unwrap();
+
+ let (name, nested) = match arg {
+ MetaItem::List(name, nested) => (name, nested),
+ _ => {
+ let attr = Attribute {
+ style: AttrStyle::Outer,
+ value: MetaItem::List("cfg_attr".into(), vec![cfg, arg]),
+ is_sugared_doc: false,
+ };
+ return (Vec::new(), Some(attr));
+ }
+ };
+
+ if name == "derive" {
+ let (derives, attr) = parse_derive_attr(reg, nested);
+ let derives = derives.into_iter()
+ .map(|d| {
+ Derive {
+ name: d,
+ cfg: Some(cfg.clone()),
+ }
+ })
+ .collect();
+ let attr = attr.map(|attr| {
+ Attribute {
+ style: AttrStyle::Outer,
+ value: MetaItem::List("cfg_attr".into(), vec![cfg, attr.value]),
+ is_sugared_doc: false,
+ }
+ });
+ (derives, attr)
+ } else {
+ let attr = Attribute {
+ style: AttrStyle::Outer,
+ value: MetaItem::List("cfg_attr".into(), vec![cfg, MetaItem::List(name, nested)]),
+ is_sugared_doc: false,
+ };
+ (Vec::new(), Some(attr))
+ }
+}
+
+/// Combine a list of cfg expressions into an attribute like `#[cfg(a)]` or
+/// `#[cfg(all(a, b, c))]`, or nothing if there are no cfg expressions.
+fn combine_cfgs(cfg: Vec<MetaItem>) -> Option<Attribute> {
+ // Flatten `all` cfgs so we don't nest `all` inside of `all`.
+ let cfg: Vec<_> = cfg.into_iter().flat_map(|cfg| {
+ let (name, nested) = match cfg {
+ MetaItem::List(name, nested) => (name, nested),
+ _ => return vec![cfg],
+ };
+ if name == "all" {
+ nested
+ } else {
+ vec![MetaItem::List(name, nested)]
+ }
+ }).collect();
+
+ let value = match cfg.len() {
+ 0 => return None,
+ 1 => cfg,
+ _ => vec![MetaItem::List("all".into(), cfg)],
+ };
+
+ Some(Attribute {
+ style: AttrStyle::Outer,
+ value: MetaItem::List("cfg".into(), value),
+ is_sugared_doc: false,
+ })
+}
diff --git a/tests/test_expand.rs b/tests/test_expand.rs
new file mode 100644
index 0000000..33effd5
--- /dev/null
+++ b/tests/test_expand.rs
@@ -0,0 +1,137 @@
+#![cfg(feature = "expand")]
+
+extern crate syn;
+use syn::*;
+
+#[macro_use]
+extern crate quote;
+use quote::Tokens;
+
+extern crate tempdir;
+use tempdir::TempDir;
+
+use std::fs::File;
+use std::io::{Read, Write};
+
+#[test]
+fn test_cfg() {
+ let original = quote! {
+ use super::*;
+
+ #[derive(A)]
+ struct P;
+
+ #[cfg_attr(feature = "q", derive(A))]
+ struct Q;
+
+ #[derive(A)]
+ #[cfg(feature = "r")]
+ struct R;
+
+ #[cfg(feature = "s1")]
+ #[cfg(all(feature = "s2", feature = "s3"))]
+ #[cfg_attr(feature = "s4", derive(A))]
+ struct S;
+ };
+
+ let expected = quote! {
+ // Unmodified from the input
+ use super::*;
+
+ type P = ();
+
+ #[cfg(feature = "q")]
+ type Q = ();
+
+ #[cfg(feature = "r")]
+ type R = ();
+
+ #[cfg(all(feature = "s1", feature = "s2", feature = "s3", feature = "s4"))]
+ type S = ();
+ };
+
+ test_expand(original, expected);
+}
+
+#[test]
+fn test_recursive() {
+ let original = quote! {
+ #[d]
+ #[cfg_attr(feature = "f", derive(Copy, B, Clone))]
+ #[e]
+ #[cfg(feature = "e")]
+ struct T;
+ };
+
+ let expected = quote! {
+ // From #[derive(A)] on struct S produced by #[derive(B)]
+ #[cfg(all(feature = "e", feature = "f", feature = "g"))]
+ type S = ();
+
+ // From #[derive(B)] on struct T
+ #[cfg(all(feature = "e", feature = "f"))]
+ impl B for T {}
+
+ // From the input
+ #[d]
+ #[cfg_attr(feature = "f", derive(Copy, Clone))]
+ #[e]
+ #[cfg(feature = "e")]
+ struct T;
+ };
+
+ test_expand(original, expected);
+}
+
+fn test_expand(original: Tokens, expected: Tokens) {
+ let dir = TempDir::new("syn").expect("create temp dir");
+ let src_path = dir.path().join("expand.in.rs");
+ let dst_path = dir.path().join("expand.rs");
+
+ // Write the src file
+ let mut src_file = File::create(&src_path).expect("create temp file");
+ src_file.write_all(original.to_string().as_bytes()).expect("write temp file");
+
+ // Run expansion
+ let mut registry = Registry::new();
+ registry.add_derive("A", expand_a);
+ registry.add_derive("B", expand_b);
+ registry.expand_file(&src_path, &dst_path).unwrap();
+
+ // Read the dst file
+ let mut expanded = String::new();
+ let mut dst_file = File::open(&dst_path).expect("open output file");
+ dst_file.read_to_string(&mut expanded).expect("read output file");
+
+ assert_eq!(expanded, expected.to_string());
+}
+
+fn expand_a(input: MacroInput) -> Result<Expanded, String> {
+ let name = &input.ident;
+ let out = quote! {
+ type #name = ();
+ };
+ Ok(Expanded {
+ new_items: parse_items(&out.to_string()).unwrap(),
+ original: None,
+ })
+}
+
+fn expand_b(input: MacroInput) -> Result<Expanded, String> {
+ assert_eq!(quote!(#input), quote! {
+ #[d]
+ #[cfg_attr(feature = "f", derive(Copy, Clone))]
+ #[e]
+ struct T;
+ });
+ let out = quote! {
+ #[cfg_attr(feature = "g", derive(A))]
+ struct S;
+
+ impl B for T {}
+ };
+ Ok(Expanded {
+ new_items: parse_items(&out.to_string()).unwrap(),
+ original: Some(input),
+ })
+}