Add proc macros for Hint and edge (#63)

* Add proc-macro crate with two macros

* Let cargo recalculate the Cargo.lock

* Add tests and refactor some code to allow testing

also the impl for parse_hint_helper_attrs now preserves order
(which is essential for testing)
This commit is contained in:
T0mstone
2021-04-07 13:51:33 +02:00
committed by GitHub
parent b7f18dfaa8
commit 5f565aeb74
8 changed files with 539 additions and 28 deletions

View File

@@ -15,3 +15,7 @@ bitflags = "1.2.1"
[dependencies.document-core]
path = "../document"
package = "graphite-document-core"
[dependencies.proc-macros]
path = "../proc-macro"
package = "graphite-proc-macros"

5
core/editor/src/hint.rs Normal file
View File

@@ -0,0 +1,5 @@
use std::collections::HashMap;
pub trait Hint {
fn hints(&self) -> HashMap<String, String>;
}

View File

@@ -1,8 +1,10 @@
#[macro_use]
mod macros;
mod color;
mod dispatcher;
mod error;
#[macro_use]
mod macros;
pub mod hint;
pub mod tools;
pub mod workspace;

View File

@@ -0,0 +1,19 @@
[package]
name = "graphite-proc-macros"
version = "0.1.0"
authors = ["Graphite Authors <contact@graphite.design>"]
edition = "2018"
publish = false
[lib]
path = "src/lib.rs"
proc-macro = true
[dependencies]
proc-macro2 = "1.0.26"
syn = "1.0.68"
quote = "1.0.9"
[dev-dependencies.editor-core]
path = "../editor"
package = "graphite-editor-core"

View File

@@ -0,0 +1,62 @@
use proc_macro2::Ident;
use syn::punctuated::Punctuated;
use syn::{Path, PathArguments, PathSegment, Token};
/// Returns `Ok(Vec<T>)` if all items are `Ok(T)`, else returns a combination of every error encountered (not just the first one)
pub fn fold_error_iter<T>(iter: impl Iterator<Item = syn::Result<T>>) -> syn::Result<Vec<T>> {
iter.fold(Ok(vec![]), |acc, x| match acc {
Ok(mut v) => x.map(|x| {
v.push(x);
v
}),
Err(mut e) => match x {
Ok(_) => Err(e),
Err(e2) => {
e.combine(e2);
Err(e)
}
},
})
}
/// Creates the path `left::right` from the idents `left` and `right`
pub fn two_path(left_ident: Ident, right_ident: Ident) -> Path {
let mut segments: Punctuated<PathSegment, Token![::]> = Punctuated::new();
segments.push(PathSegment {
ident: left_ident,
arguments: PathArguments::None,
});
segments.push(PathSegment {
ident: right_ident,
arguments: PathArguments::None,
});
Path { leading_colon: None, segments }
}
#[cfg(test)]
mod tests {
use super::*;
use quote::ToTokens;
use syn::spanned::Spanned;
#[test]
fn test_fold_error_iter() {
let res = fold_error_iter(vec![Ok(()), Ok(())].into_iter());
assert!(res.is_ok());
let _span = quote::quote! { "" }.span();
let res = fold_error_iter(vec![Ok(()), Err(syn::Error::new(_span, "err1")), Err(syn::Error::new(_span, "err2"))].into_iter());
assert!(res.is_err());
let err = res.unwrap_err();
let mut check_err = syn::Error::new(_span, "err1");
check_err.combine(syn::Error::new(_span, "err2"));
assert_eq!(err.to_compile_error().to_string(), check_err.to_compile_error().to_string());
}
#[test]
fn test_two_path() {
let _span = quote::quote! { "" }.span();
assert_eq!(two_path(Ident::new("a", _span), Ident::new("b", _span)).to_token_stream().to_string(), "a :: b");
}
}

264
core/proc-macro/src/lib.rs Normal file
View File

@@ -0,0 +1,264 @@
mod helpers;
mod structs;
use crate::helpers::{fold_error_iter, two_path};
use crate::structs::{AttrInnerKeyStringMap, AttrInnerSingleString};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use syn::{parse_macro_input, Attribute, Data, DeriveInput, LitStr, Variant};
fn parse_hint_helper_attrs(attrs: &[Attribute]) -> syn::Result<(Vec<LitStr>, Vec<LitStr>)> {
fold_error_iter(
attrs
.iter()
.filter(|a| a.path.get_ident().map_or(false, |i| i == "hint"))
.map(|attr| syn::parse2::<AttrInnerKeyStringMap>(attr.tokens.clone())),
)
.and_then(|v: Vec<AttrInnerKeyStringMap>| {
fold_error_iter(AttrInnerKeyStringMap::multi_into_iter(v).map(|(k, mut v)| match v.len() {
0 => panic!("internal error: a key without values was somehow inserted into the hashmap"),
1 => {
let single_val = v.pop().unwrap();
Ok((LitStr::new(&k.to_string(), Span::call_site()), single_val))
}
_ => {
// the first value is ok, the other ones should error
let after_first = v.into_iter().skip(1);
// this call to fold_error_iter will always return Err with a combined error
fold_error_iter(after_first.map(|lit| Err(syn::Error::new(lit.span(), format!("value for key {} was already given", k))))).map(|_: Vec<()>| unreachable!())
}
}))
})
.map(|v| v.into_iter().unzip())
}
fn derive_hint_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> {
let input = syn::parse2::<DeriveInput>(input_item)?;
let ident = input.ident;
match input.data {
Data::Enum(data) => {
let variants = data.variants.iter().map(|var: &Variant| two_path(ident.clone(), var.ident.clone())).collect::<Vec<_>>();
let hint_result = fold_error_iter(data.variants.into_iter().map(|var: Variant| parse_hint_helper_attrs(&var.attrs)));
hint_result.map(|hints: Vec<(Vec<LitStr>, Vec<LitStr>)>| {
let (keys, values): (Vec<Vec<LitStr>>, Vec<Vec<LitStr>>) = hints.into_iter().unzip();
let cap: Vec<usize> = keys.iter().map(|v| v.len()).collect();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
match self {
#(
#variants { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
)*
}
}
}
}
})
}
Data::Struct(_) | Data::Union(_) => {
let hint_result = parse_hint_helper_attrs(&input.attrs);
hint_result.map(|(keys, values)| {
let cap = keys.len();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
}
}
})
}
}
}
/// Derive the `Hint` trait
///
/// # Example
/// ```
/// # use graphite_proc_macros::Hint;
/// # use editor_core::hint::Hint;
///
/// #[derive(Hint)]
/// pub enum StateMachine {
/// #[hint(rmb = "foo", lmb = "bar")]
/// Ready,
/// #[hint(alt = "baz")]
/// RMBDown,
/// // no hint (also ok)
/// LMBDown
/// }
/// ```
#[proc_macro_derive(Hint, attributes(hint))]
pub fn derive_hint(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_hint_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// The `edge` proc macro does nothing, it is intended for use with an external tool
///
/// # Example
/// ```ignore
/// match (example_tool_state, event) {
/// (ToolState::Ready, Event::MouseDown(mouse_state)) if *mouse_state == MouseState::Left => {
/// #[edge("LMB Down")]
/// ToolState::Pending
/// }
/// (SelectToolState::Pending, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
/// #[edge("LMB Up: Select Object")]
/// SelectToolState::Ready
/// }
/// (SelectToolState::Pending, Event::MouseMove(x,y)) => {
/// #[edge("Mouse Move")]
/// SelectToolState::TransformSelected
/// }
/// (SelectToolState::TransformSelected, Event::MouseMove(x,y)) => {
/// #[egde("Mouse Move")]
/// SelectToolState::TransformSelected
/// }
/// (SelectToolState::TransformSelected, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
/// #[edge("LMB Up")]
/// SelectToolState::Ready
/// }
/// (state, _) => {
/// // Do nothing
/// state
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn edge(attr: TokenStream, item: TokenStream) -> TokenStream {
// to make sure that only `#[edge("string")]` is allowed
let _verify = parse_macro_input!(attr as AttrInnerSingleString);
item
}
#[cfg(test)]
mod tests {
use super::*;
fn ts_assert_eq(l: TokenStream2, r: TokenStream2) {
// not sure if this is the best way of doing things but if two TokenStreams are equal, their `to_string` is also equal
// so there are at least no false negatives
assert_eq!(l.to_string(), r.to_string());
}
#[test]
fn test_derive_hint() {
let res = derive_hint_impl(quote::quote! {
#[hint(key1="val1",key2="val2",)]
struct S { a: u8, b: String, c: bool }
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for S {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
hm.insert("key1".to_string(), "val1".to_string());
hm.insert("key2".to_string(), "val2".to_string());
hm
}
}
},
);
let res = derive_hint_impl(quote::quote! {
enum E {
#[hint(key1="val1",key2="val2",)]
S { a: u8, b: String, c: bool },
#[hint(key3="val3")]
X,
Y
}
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for E {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
match self {
E::S { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
hm.insert("key1".to_string(), "val1".to_string());
hm.insert("key2".to_string(), "val2".to_string());
hm
}
E::X { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(1usize);
hm.insert("key3".to_string(), "val3".to_string());
hm
}
E::Y { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(0usize);
hm
}
}
}
}
},
);
let res = derive_hint_impl(quote::quote! {
union NoHint {}
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for NoHint {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(0usize);
hm
}
}
},
);
let res = derive_hint_impl(quote::quote! {
#[hint(a="1", a="2")]
struct S;
});
assert!(res.is_err());
let res = derive_hint_impl(quote::quote! {
#[hint(a="1")]
#[hint(b="2")]
struct S;
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for S {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
hm.insert("a".to_string(), "1".to_string());
hm.insert("b".to_string(), "2".to_string());
hm
}
}
},
)
}
// note: edge needs no testing since AttrInnerSingleString has testing and that's all you'd need to test with edge
}

View File

@@ -0,0 +1,144 @@
use proc_macro2::Ident;
use std::collections::HashMap;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::Paren;
use syn::{parenthesized, LitStr, Token};
/// Parses `("some text")`
pub struct AttrInnerSingleString {
_paren_token: Paren,
pub content: LitStr,
}
impl Parse for AttrInnerSingleString {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let _paren_token = parenthesized!(content in input);
Ok(Self {
_paren_token,
content: content.parse()?,
})
}
}
/// Parses `key="value"`
pub struct KeyEqString {
key: Ident,
_eq_token: Token![=],
lit: LitStr,
}
impl Parse for KeyEqString {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
key: input.parse()?,
_eq_token: input.parse()?,
lit: input.parse()?,
})
}
}
/// Parses `(key="value", key="value", …)`
pub struct AttrInnerKeyStringMap {
_paren_token: Paren,
parts: Punctuated<KeyEqString, Token![,]>,
}
impl Parse for AttrInnerKeyStringMap {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let _paren_token = parenthesized!(content in input);
Ok(Self {
_paren_token,
parts: Punctuated::parse_terminated(&content)?,
})
}
}
impl AttrInnerKeyStringMap {
pub fn multi_into_iter(iter: impl IntoIterator<Item = Self>) -> impl Iterator<Item = (Ident, Vec<LitStr>)> {
use std::collections::hash_map::Entry;
let mut res = Vec::<(Ident, Vec<LitStr>)>::new();
let mut idx = HashMap::<Ident, usize>::new();
for part in iter.into_iter().flat_map(|x: Self| x.parts) {
match idx.entry(part.key) {
Entry::Occupied(occ) => {
res[*occ.get()].1.push(part.lit);
}
Entry::Vacant(vac) => {
let ident = vac.key().clone();
vac.insert(res.len());
res.push((ident, vec![part.lit]));
}
}
}
res.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attr_inner_single_string() {
let res = syn::parse2::<AttrInnerSingleString>(quote::quote! {
("a string literal")
});
assert!(res.is_ok());
assert_eq!(res.ok().unwrap().content.value(), "a string literal");
let res = syn::parse2::<AttrInnerSingleString>(quote::quote! {
wrong, "stuff"
});
assert!(res.is_err());
}
#[test]
fn key_eq_string() {
let res = syn::parse2::<KeyEqString>(quote::quote! {
key="value"
});
assert!(res.is_ok());
let res = res.ok().unwrap();
assert_eq!(res.key, "key");
assert_eq!(res.lit.value(), "value");
let res = syn::parse2::<KeyEqString>(quote::quote! {
wrong, "stuff"
});
assert!(res.is_err());
}
#[test]
fn attr_inner_key_string_map() {
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
(key="value", key2="value2")
});
assert!(res.is_ok());
let res = res.ok().unwrap();
for (item, (k, v)) in res.parts.into_iter().zip(vec![("key", "value"), ("key2", "value2")]) {
assert_eq!(item.key, k);
assert_eq!(item.lit.value(), v);
}
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
(key="value", key2="value2",)
});
assert!(res.is_ok());
let res = res.ok().unwrap();
for (item, (k, v)) in res.parts.into_iter().zip(vec![("key", "value"), ("key2", "value2")]) {
assert_eq!(item.key, k);
assert_eq!(item.lit.value(), v);
}
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
wrong, "stuff"
});
assert!(res.is_err());
}
}