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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 539 additions and 28 deletions

63
Cargo.lock generated
View file

@ -58,9 +58,20 @@ version = "0.1.0"
dependencies = [
"bitflags",
"graphite-document-core",
"graphite-proc-macros",
"log",
]
[[package]]
name = "graphite-proc-macros"
version = "0.1.0"
dependencies = [
"graphite-editor-core",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "graphite-renderer-core"
version = "0.1.0"
@ -78,18 +89,18 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.49"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821"
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kurbo"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb348d766edbac91ba1eb83020d96f4f8867924d194393083c15a51f185e6a82"
checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42"
dependencies = [
"arrayvec",
]
@ -111,9 +122,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.24"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
dependencies = [
"unicode-xid",
]
@ -135,9 +146,9 @@ checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
[[package]]
name = "syn"
version = "1.0.64"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f"
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
dependencies = [
"proc-macro2",
"quote",
@ -152,9 +163,9 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "wasm-bindgen"
version = "0.2.72"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe"
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
@ -162,9 +173,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.72"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3"
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
dependencies = [
"bumpalo",
"lazy_static",
@ -177,9 +188,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.22"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73157efb9af26fb564bb59a009afd1c7c334a44db171d280690d0c3faaec3468"
checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
@ -189,9 +200,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.72"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b"
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -199,9 +210,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.72"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d"
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
dependencies = [
"proc-macro2",
"quote",
@ -212,15 +223,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.72"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa"
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f002ea97b5abdb19aafd48cbb5a0a7f6931cf36ea05a0a46ccc95d9f4c2cf43"
checksum = "e972e914de63aa53bd84865e54f5c761bd274d48e5be3a6329a662c0386aa67a"
dependencies = [
"console_error_panic_hook",
"js-sys",
@ -232,9 +243,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a6c0bd3933daf64c78fc25a7452530f79fa7e21f77fa03d608d1e988a66735"
checksum = "ea6153a8f9bf24588e9f25c87223414fff124049f68d3a442a0f0eab4768a8b6"
dependencies = [
"proc-macro2",
"quote",
@ -242,9 +253,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.49"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310"
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
dependencies = [
"js-sys",
"wasm-bindgen",

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());
}
}