mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-09-01 02:37:21 +00:00
Add tests to the Ellipse, Artboard, and Fill tools (#2181)
* Add ellipse tests * Add tests for fill tool and re-enable some other tests * Code review * Fix Rust crate advisory --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
1190e82322
commit
b171eeba84
22 changed files with 1097 additions and 236 deletions
|
@ -5,7 +5,10 @@ use convert_case::{Case, Casing};
|
|||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use proc_macro_crate::FoundCrate;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Error, Ident, Token, WhereClause, WherePredicate};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse_quote, Error, Ident, PatIdent, Token, WhereClause, WherePredicate};
|
||||
static NODE_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> {
|
||||
|
@ -250,6 +253,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
|
||||
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
|
||||
|
||||
let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, &graphene_core, &identifier);
|
||||
Ok(quote! {
|
||||
/// Underlying implementation for [#struct_name]
|
||||
#[inline]
|
||||
|
@ -265,6 +269,9 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
#[doc(inline)]
|
||||
pub use #mod_name::#struct_name;
|
||||
|
||||
#[doc(hidden)]
|
||||
#node_input_accessor
|
||||
|
||||
#[doc(hidden)]
|
||||
mod #mod_name {
|
||||
use super::*;
|
||||
|
@ -326,6 +333,93 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
})
|
||||
}
|
||||
|
||||
/// Generates strongly typed utilites to access inputs
|
||||
fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::GenericParam], field_idents: &[&PatIdent], graphene_core: &TokenStream2, identifier: &TokenStream2) -> TokenStream2 {
|
||||
if parsed.attributes.skip_impl {
|
||||
return quote! {};
|
||||
}
|
||||
let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake));
|
||||
|
||||
let (mut modified, mut generic_collector) = FilterUsedGenerics::new(fn_generics);
|
||||
|
||||
let mut generated_input_accessor = Vec::new();
|
||||
for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() {
|
||||
let mut ty = match parsed_input {
|
||||
ParsedField::Regular { ty, .. } => ty,
|
||||
ParsedField::Node { output_type, .. } => output_type,
|
||||
}
|
||||
.clone();
|
||||
|
||||
// We only want the necessary generics.
|
||||
let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty);
|
||||
// TODO: figure out a better name that doesn't conflict with so many types
|
||||
let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal));
|
||||
let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter());
|
||||
|
||||
// Only create structs with phantom data where necessary.
|
||||
generated_input_accessor.push(if phantom_data_declerations.is_empty() {
|
||||
quote! {
|
||||
pub struct #struct_name;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
pub struct #struct_name <#(#used),*>{
|
||||
#(#phantom_data_declerations,)*
|
||||
}
|
||||
}
|
||||
});
|
||||
generated_input_accessor.push(quote! {
|
||||
impl <#(#used),*> #graphene_core::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> {
|
||||
const INDEX: usize = #input_index;
|
||||
fn identifier() -> &'static str {
|
||||
protonode_identifier()
|
||||
}
|
||||
type Result = #ty;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
quote! {
|
||||
pub mod #inputs_module_name {
|
||||
use super::*;
|
||||
|
||||
pub fn protonode_identifier() -> &'static str {
|
||||
// Storing the string in a once lock should reduce allocations (since we call this in a loop)?
|
||||
static NODE_NAME: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
NODE_NAME.get_or_init(|| #identifier )
|
||||
}
|
||||
#(#generated_input_accessor)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// It is necessary to generate PhantomData for each fn generic to avoid compiler errors.
|
||||
fn generate_phantom_data<'a>(fn_generics: impl Iterator<Item = &'a crate::GenericParam>) -> (Vec<TokenStream2>, Vec<TokenStream2>) {
|
||||
let mut phantom_data_declerations = Vec::new();
|
||||
let mut fn_generic_params = Vec::new();
|
||||
|
||||
for fn_generic_param in fn_generics {
|
||||
let field_name = format_ident!("phantom_{}", phantom_data_declerations.len());
|
||||
|
||||
match fn_generic_param {
|
||||
crate::GenericParam::Lifetime(lifetime_param) => {
|
||||
let lifetime = &lifetime_param.lifetime;
|
||||
|
||||
fn_generic_params.push(quote! {#lifetime});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<&#lifetime ()>})
|
||||
}
|
||||
crate::GenericParam::Type(type_param) => {
|
||||
let generic_name = &type_param.ident;
|
||||
|
||||
fn_generic_params.push(quote! {#generic_name});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<#generic_name>});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(fn_generic_params, phantom_data_declerations)
|
||||
}
|
||||
|
||||
fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], struct_name: &Ident, identifier: &TokenStream2) -> Result<TokenStream2, syn::Error> {
|
||||
if parsed.attributes.skip_impl {
|
||||
return Ok(quote!());
|
||||
|
@ -469,3 +563,83 @@ fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type {
|
|||
LifetimeReplacer(lifetime).visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
|
||||
/// Get only the necessary generics.
|
||||
struct FilterUsedGenerics {
|
||||
all: Vec<crate::GenericParam>,
|
||||
used: Vec<bool>,
|
||||
}
|
||||
|
||||
impl VisitMut for FilterUsedGenerics {
|
||||
fn visit_lifetime_mut(&mut self, used_lifetime: &mut syn::Lifetime) {
|
||||
for (generic, used) in self.all.iter().zip(self.used.iter_mut()) {
|
||||
let crate::GenericParam::Lifetime(lifetime_param) = generic else { continue };
|
||||
if used_lifetime == &lifetime_param.lifetime {
|
||||
*used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_path_mut(&mut self, path: &mut syn::Path) {
|
||||
for (index, (generic, used)) in self.all.iter().zip(self.used.iter_mut()).enumerate() {
|
||||
let crate::GenericParam::Type(type_param) = generic else { continue };
|
||||
if path.leading_colon.is_none() && !path.segments.is_empty() && path.segments[0].arguments.is_none() && path.segments[0].ident == type_param.ident {
|
||||
*used = true;
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
path.segments[0].ident = format_ident!("G{index}");
|
||||
}
|
||||
}
|
||||
for mut el in Punctuated::pairs_mut(&mut path.segments) {
|
||||
self.visit_path_segment_mut(el.value_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterUsedGenerics {
|
||||
fn new(fn_generics: &[crate::GenericParam]) -> (Vec<crate::GenericParam>, Self) {
|
||||
let mut all_possible_generics = fn_generics.to_vec();
|
||||
// The 'n lifetime may also be needed; we must add it in
|
||||
all_possible_generics.insert(0, syn::GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new("'n", proc_macro2::Span::call_site()))));
|
||||
|
||||
let modified = all_possible_generics
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(index, mut generic)| {
|
||||
let crate::GenericParam::Type(type_param) = &mut generic else { return generic };
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
type_param.ident = format_ident!("G{index}");
|
||||
generic
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let generic_collector = Self {
|
||||
used: vec![false; all_possible_generics.len()],
|
||||
all: all_possible_generics,
|
||||
};
|
||||
|
||||
(modified, generic_collector)
|
||||
}
|
||||
|
||||
fn used<'a>(&'a self, modified: &'a [crate::GenericParam]) -> impl Iterator<Item = &'a crate::GenericParam> {
|
||||
modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value)
|
||||
}
|
||||
|
||||
fn filter_unnecessary_generics(&mut self, modified: &mut Vec<syn::GenericParam>, ty: &mut Type) -> Vec<syn::GenericParam> {
|
||||
self.used.fill(false);
|
||||
|
||||
// Find out which generics are necessary to support the node input
|
||||
self.visit_type_mut(ty);
|
||||
|
||||
// Sometimes generics may reference other generics. This is a non-optimal way of dealing with that.
|
||||
for _ in 0..=self.all.len() {
|
||||
for (index, item) in modified.iter_mut().enumerate() {
|
||||
if self.used[index] {
|
||||
self.visit_generic_param_mut(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.used(&*modified).cloned().collect()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue