diff --git a/Cargo.lock b/Cargo.lock index 843ce2ff..293dceca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,18 @@ dependencies = [ "syn", ] +[[package]] +name = "cgp-macro-tests" +version = "0.7.0" +dependencies = [ + "cgp", + "cgp-macro-core", + "cgp-macro-test-util", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cgp-monad" version = "0.7.0" @@ -252,9 +264,13 @@ name = "cgp-tests" version = "0.7.0" dependencies = [ "cgp", + "cgp-macro-core", "cgp-macro-test-util", "futures", "insta", + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cf5a47e4..8fe89c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "crates/standalone/error/cgp-error-std", "crates/tests/cgp-tests", + "crates/tests/cgp-macro-tests", ] [workspace.package] diff --git a/crates/macros/cgp-macro-core/src/types/attributes/cgp_impl_attributes.rs b/crates/macros/cgp-macro-core/src/types/attributes/cgp_impl_attributes.rs index 5ced2be5..3ac0b77d 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/cgp_impl_attributes.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/cgp_impl_attributes.rs @@ -7,7 +7,7 @@ use crate::types::attributes::{ DefaultImplAttribute, DefaultImplAttributes, UseProviderAttribute, UseProviderAttributes, UseTypeAttribute, UseTypeAttributes, UsesAttributes, }; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; #[derive(Default)] pub struct CgpImplAttributes { @@ -27,7 +27,7 @@ impl CgpImplAttributes { match ident.to_string().as_ref() { "uses" => { let uses = attribute.parse_args_with( - Punctuated::::parse_terminated, + Punctuated::::parse_terminated, )?; parsed_attributes.uses.imports.extend(uses); diff --git a/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs b/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs index f1708c71..b9ad4e7b 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs @@ -3,13 +3,13 @@ use syn::token::In; use syn::{Generics, ItemImpl, Type}; use crate::parse_internal; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; use crate::types::path::UniPathOrType; pub struct DefaultImplAttribute { pub key_type: UniPathOrType, pub in_token: In, - pub namespace: IdentWithTypeArgs, + pub namespace: PathWithTypeArgs, } impl DefaultImplAttribute { @@ -23,7 +23,7 @@ impl DefaultImplAttribute { namespace_trait_path .type_args - .make_args() + .args .push(parse_internal!(__Components__)); let mut generics = provider_generics.clone(); diff --git a/crates/macros/cgp-macro-core/src/types/attributes/function.rs b/crates/macros/cgp-macro-core/src/types/attributes/function.rs index 3e97e52e..db64ac82 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/function.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/function.rs @@ -5,13 +5,13 @@ use syn::{Attribute, GenericParam, TypeParamBound, WherePredicate}; use crate::types::attributes::{ UseProviderAttribute, UseProviderAttributes, UseTypeAttribute, UseTypeAttributes, }; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; #[derive(Default)] pub struct FunctionAttributes { pub extend: Vec, pub extend_where: Vec, - pub uses: Vec, + pub uses: Vec, pub use_type: UseTypeAttributes, pub use_provider: UseProviderAttributes, pub impl_generics: Vec, @@ -35,9 +35,8 @@ impl FunctionAttributes { parsed_attributes.extend_where.extend(where_predicates); } else if ident == "uses" { - let uses = attribute.parse_args_with( - Punctuated::::parse_terminated, - )?; + let uses = attribute + .parse_args_with(Punctuated::::parse_terminated)?; parsed_attributes.uses.extend(uses); } else if ident == "use_type" { diff --git a/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs b/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs index 70a64f64..4b845c76 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs @@ -4,14 +4,14 @@ use syn::token::In; use crate::exports::RedirectLookup; use crate::functions::parse_internal; -use crate::types::ident::{IdentWithTypeArgs, IdentWithTypeGenerics}; +use crate::types::ident::{IdentWithTypeGenerics, PathWithTypeArgs}; use crate::types::path::UniPath; #[derive(Clone)] pub struct PrefixAttribute { pub path: UniPath, pub _in_token: In, - pub namespace: IdentWithTypeArgs, + pub namespace: PathWithTypeArgs, } impl PrefixAttribute { @@ -22,7 +22,7 @@ impl PrefixAttribute { let mut namespace = self.namespace.clone(); namespace .type_args - .make_args() + .args .push(parse_internal!(__Components__)); let mut path = self.path.clone(); diff --git a/crates/macros/cgp-macro-core/src/types/attributes/use_provider/attribute.rs b/crates/macros/cgp-macro-core/src/types/attributes/use_provider/attribute.rs index 6824f94e..688ea63e 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/use_provider/attribute.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/use_provider/attribute.rs @@ -4,13 +4,13 @@ use syn::token::{Colon, Plus}; use syn::{Type, TypeParamBound, WherePredicate}; use crate::parse_internal; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; pub struct UseProviderAttribute { pub context_type: Type, pub provider_type: Type, pub colon: Colon, - pub provider_trait_bounds: Punctuated, + pub provider_trait_bounds: Punctuated, } impl UseProviderAttribute { @@ -24,7 +24,7 @@ impl UseProviderAttribute { let mut bound = bound.clone(); bound .type_args - .make_args() + .args .insert(0, parse_internal!(#context_type)); bounds.push(parse_internal!(#bound)); diff --git a/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs b/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs index 617b8679..187d691e 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs @@ -4,12 +4,12 @@ use syn::{Ident, Type, braced}; use crate::parse_internal; use crate::types::attributes::UseTypeIdent; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::{IdentWithTypeArgs, PathWithTypeArgs}; #[derive(Clone)] pub struct UseTypeAttribute { pub context_type: Type, - pub trait_path: IdentWithTypeArgs, + pub trait_path: PathWithTypeArgs, pub type_idents: Vec, } @@ -34,6 +34,12 @@ impl Parse for UseTypeAttribute { let (context_type, body) = if input.peek(At) { let _: At = input.parse()?; + // The context type is followed by a `::`-separated trait path, so it + // must parse only a single identifier head. This is the one place + // that deliberately keeps `IdentWithTypeArgs` rather than the + // otherwise-dominant `PathWithTypeArgs`: a path parser is greedy + // across `::` and would silently consume the trailing `::Trait::Type` + // here, with no parse error. Do NOT swap this for `PathWithTypeArgs`. let context_type: Type = input.parse::()?.into(); let _: Colon = input.parse()?; @@ -51,7 +57,7 @@ impl Parse for UseTypeAttribute { let trait_path = if body.peek(Lt) { let _: Lt = body.parse()?; - let trait_path: IdentWithTypeArgs = body.parse()?; + let trait_path: PathWithTypeArgs = body.parse()?; let _: Gt = body.parse()?; trait_path } else { diff --git a/crates/macros/cgp-macro-core/src/types/attributes/uses.rs b/crates/macros/cgp-macro-core/src/types/attributes/uses.rs index 9ce76a09..a382c381 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/uses.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/uses.rs @@ -4,11 +4,11 @@ use syn::token::Plus; use crate::parse_internal; use crate::traits::ToTypeParamBounds; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; #[derive(Default)] pub struct UsesAttributes { - pub imports: Vec, + pub imports: Vec, } impl ToTypeParamBounds for UsesAttributes { diff --git a/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/item.rs b/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/item.rs index 284fba18..ce419b64 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/item.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/item.rs @@ -15,7 +15,7 @@ impl PreprocessedCgpComponent { let component_name = &self.args.component_name; EmptyStruct { ident: component_name.ident.clone(), - generics: component_name.type_generics.generics.clone(), + generics: component_name.type_generics.to_generics(), } } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs b/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs index b5a6dca3..906f5ef8 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs @@ -8,7 +8,7 @@ use syn::{Error, Ident, ImplItem, ItemImpl, Type}; use crate::functions::{parse_internal, to_snake_case_ident}; use crate::types::cgp_impl::{CgpProviderOrBareImpl, ImplArgs}; use crate::types::cgp_provider::{ItemCgpProvider, ProviderArgs}; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; use crate::visitors::{ ReplaceSelfReceiverVisitor, ReplaceSelfTypeVisitor, ReplaceSelfValueVisitor, }; @@ -17,7 +17,7 @@ pub struct LoweredCgpImpl { pub args: ImplArgs, pub item_impl: ItemImpl, pub context_type: Type, - pub provider_trait_path: IdentWithTypeArgs, + pub provider_trait_path: PathWithTypeArgs, pub default_impls: Vec, } @@ -82,7 +82,7 @@ impl LoweredCgpImpl { provider_trait_path .type_args - .make_args() + .args .insert(0, parse_internal!(#context_type)); out_impl.trait_ = Some(( diff --git a/crates/macros/cgp-macro-core/src/types/cgp_provider/item.rs b/crates/macros/cgp-macro-core/src/types/cgp_provider/item.rs index 6ba5c312..a27134b5 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_provider/item.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_provider/item.rs @@ -5,7 +5,7 @@ use syn::{Error, Ident, ItemImpl, Type}; use crate::functions::parse_internal; use crate::types::cgp_provider::{LoweredCgpProvider, ProviderArgs}; use crate::types::empty_struct::EmptyStruct; -use crate::types::ident::{IdentWithTypeArgs, IdentWithTypeGenerics}; +use crate::types::ident::{IdentWithTypeGenerics, PathWithTypeArgs}; use crate::types::provider_impl::ItemProviderImpl; pub struct ItemCgpProvider { @@ -37,11 +37,11 @@ impl ItemCgpProvider { Error::new(item_impl.span(), "expect provider trait name to be present") })?; - let provider_trait: IdentWithTypeArgs = + let provider_trait: PathWithTypeArgs = parse_internal(provider_trait_path.to_token_stream())?; let component_ident = Ident::new( - &format!("{}Component", provider_trait.ident), + &format!("{}Component", provider_trait.ident()), provider_trait.span(), ); @@ -61,7 +61,7 @@ impl ItemCgpProvider { let provider_struct = EmptyStruct { ident: provider_type.ident.clone(), - generics: provider_type.type_generics.generics.clone(), + generics: provider_type.type_generics.to_generics(), }; Ok(Some(provider_struct)) diff --git a/crates/macros/cgp-macro-core/src/types/cgp_provider/provider_impl_args.rs b/crates/macros/cgp-macro-core/src/types/cgp_provider/provider_impl_args.rs index e6d31c3f..116c28a0 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_provider/provider_impl_args.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_provider/provider_impl_args.rs @@ -3,9 +3,9 @@ use quote::{ToTokens, quote}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::token::Comma; -use syn::{Error, GenericArgument, Lifetime, Type}; +use syn::{Error, Lifetime, Type}; -use crate::types::generics::GenericArguments; +use crate::types::ident::{TypeArg, TypeArgs}; pub struct ProviderImplArgs { pub context_type: Type, @@ -18,30 +18,28 @@ pub enum ProviderImplArg { } impl ProviderImplArgs { - pub fn from_generic_args(generic_args: &GenericArguments) -> syn::Result { + pub fn from_generic_args(generic_args: &TypeArgs) -> syn::Result { let mut impl_args: Punctuated = Punctuated::new(); let mut context_type: Option = None; - if let Some(args) = &generic_args.args { - for arg in &args.args { - match arg { - GenericArgument::Lifetime(life) => { - impl_args.push(ProviderImplArg::Life(life.clone())); - } - GenericArgument::Type(ty) => { - if context_type.is_none() { - context_type = Some(ty.clone()); - } else { - impl_args.push(ProviderImplArg::Type(ty.clone())); - } - } - _ => { - return Err(Error::new( - arg.span(), - format!("unsupported type argument: {:?}", arg), - )); + for arg in &generic_args.args { + match arg { + TypeArg::Lifetime(life) => { + impl_args.push(ProviderImplArg::Life(life.clone())); + } + TypeArg::Type(ty) => { + if context_type.is_none() { + context_type = Some(ty.clone()); + } else { + impl_args.push(ProviderImplArg::Type(ty.clone())); } } + TypeArg::Const(expr) => { + return Err(Error::new( + expr.span(), + "const arguments are not supported in provider impl trait arguments", + )); + } } } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs index 965dd10c..8a388575 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs @@ -2,7 +2,7 @@ use syn::{Generics, Ident, Type}; use crate::parse_internal; use crate::types::delegate_component::{EvalDelegateEntry, EvaluatedDelegateEntry}; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; pub trait EvalForEntries { fn eval_for_entries(&self, table_type: &Type) -> syn::Result>; @@ -17,7 +17,7 @@ pub struct EvaluatedForEntry { pub table_type: Type, pub for_key: Ident, pub for_value: Ident, - pub namespace: IdentWithTypeArgs, + pub namespace: PathWithTypeArgs, pub mapping_key: Type, pub mapping_value: Type, } @@ -47,17 +47,18 @@ impl EvalDelegateEntry for EvaluatedForEntry { let table_type = &self.table_type; let namespace_trait: Type = { - let namespace_ident = &self.namespace.ident; - let mut namespace_generics = self.namespace.type_args.clone(); - let namespace_generic_args = &mut namespace_generics.make_args(); - - namespace_generic_args.push(parse_internal!(#table_type)); - - namespace_generic_args.push(parse_internal! { - Delegate = #mapping_value - }); - - parse_internal!( #namespace_ident #namespace_generics ) + // The namespace argument list is extended with the table type and a + // `Delegate = ..` associated binding. The binding cannot live inside + // a `TypeArgs` (which faithfully rejects associated bindings), so the + // trait bound is reconstructed directly from the parsed path and its + // existing arguments. + let namespace_path = &self.namespace.path; + + let existing_args = self.namespace.type_args.args.iter(); + + parse_internal! { + #namespace_path < #( #existing_args, )* #table_type, Delegate = #mapping_value > + } }; let mut generics = self.generics.clone(); diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs index d483312c..c948f0ce 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs @@ -8,7 +8,7 @@ use crate::types::delegate_component::{ EvaluatedDelegateEntry, EvaluatedForEntry, NormalDelegateMapping, eval_delegate_entries_via_for, }; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; #[derive(Debug, Clone)] pub struct ForDelegateStatement { @@ -19,7 +19,7 @@ pub struct ForDelegateStatement { pub value: Ident, pub gt: Gt, pub in_token: In, - pub namespace: IdentWithTypeArgs, + pub namespace: PathWithTypeArgs, pub where_clause: Option, pub mappings: Punctuated, } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/table/main.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/table/main.rs index f6d0b53d..5a2b8dcc 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/table/main.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/table/main.rs @@ -58,7 +58,7 @@ impl DelegateTable { parse_internal(self.table_type.to_token_stream())?; item_structs.push(EmptyStruct { ident: struct_type.ident, - generics: struct_type.type_generics.generics, + generics: struct_type.type_generics.to_generics(), }); } diff --git a/crates/macros/cgp-macro-core/src/types/generics/arguments.rs b/crates/macros/cgp-macro-core/src/types/generics/arguments.rs deleted file mode 100644 index f0284ea9..00000000 --- a/crates/macros/cgp-macro-core/src/types/generics/arguments.rs +++ /dev/null @@ -1,49 +0,0 @@ -use proc_macro2::TokenStream; -use quote::ToTokens; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::token::{Comma, Lt}; -use syn::{AngleBracketedGenericArguments, GenericArgument, parse_quote}; - -use crate::types::generics::TypeGenerics; - -#[derive(Debug, Clone, Default)] -pub struct GenericArguments { - pub args: Option, -} - -impl GenericArguments { - pub fn make_args(&mut self) -> &mut Punctuated { - &mut self.args.get_or_insert_with(|| parse_quote!(<>)).args - } -} - -impl Parse for GenericArguments { - fn parse(input: ParseStream) -> syn::Result { - if input.peek(Lt) { - let args = input.parse()?; - Ok(Self { args: Some(args) }) - } else { - Ok(Self { args: None }) - } - } -} - -impl ToTokens for GenericArguments { - fn to_tokens(&self, tokens: &mut TokenStream) { - if let Some(args) = &self.args { - args.to_tokens(tokens); - } - } -} - -impl From for GenericArguments { - fn from(generics: TypeGenerics) -> Self { - if generics.params.is_empty() { - Self { args: None } - } else { - let args = parse_quote!(#generics); - Self { args: Some(args) } - } - } -} diff --git a/crates/macros/cgp-macro-core/src/types/generics/mod.rs b/crates/macros/cgp-macro-core/src/types/generics/mod.rs index 85037d78..9d6eafc8 100644 --- a/crates/macros/cgp-macro-core/src/types/generics/mod.rs +++ b/crates/macros/cgp-macro-core/src/types/generics/mod.rs @@ -1,7 +1,5 @@ -mod arguments; mod impl_generics; mod type_generics; -pub use arguments::*; pub use impl_generics::*; pub use type_generics::*; diff --git a/crates/macros/cgp-macro-core/src/types/generics/type_generics.rs b/crates/macros/cgp-macro-core/src/types/generics/type_generics.rs index 664b21ab..474a05d9 100644 --- a/crates/macros/cgp-macro-core/src/types/generics/type_generics.rs +++ b/crates/macros/cgp-macro-core/src/types/generics/type_generics.rs @@ -7,6 +7,18 @@ use syn::{Error, Generics}; use crate::functions::parse_internal; +/// A validated newtype around [`syn::Generics`] restricted to a definition-site +/// generic list (no bounds). Because it `Deref`s to [`syn::Generics`], the full +/// `syn` API (`split_for_impl`, mutating `params`, …) is available, and its +/// `TryFrom<&Generics>` adapts a generic list already parsed off an item. +/// +/// Prefer this when adapting or manipulating an existing `syn::Generics`. When +/// instead *parsing tokens* and you want strict, kind-classified parameters, +/// prefer [`TypeGenericParams`]. The two are intentionally kept separate; see +/// [`TypeGenericParams`] for the full rationale (notably, `TryFrom` here +/// normalizes a `const N: T` parameter down to a bare `N`). +/// +/// [`TypeGenericParams`]: crate::types::ident::TypeGenericParams #[derive(Debug, Clone, Default)] pub struct TypeGenerics { pub generics: Generics, diff --git a/crates/macros/cgp-macro-core/src/types/ident/angle_bracketed.rs b/crates/macros/cgp-macro-core/src/types/ident/angle_bracketed.rs new file mode 100644 index 00000000..b6458f47 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/angle_bracketed.rs @@ -0,0 +1,43 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::token::{Comma, Gt, Lt}; + +/// Parse an optional `< T, T, ... >` comma-separated list. An absent leading +/// `<` yields an empty list, matching how both the type-argument and the +/// type-generic-parameter lists treat the bare (no angle brackets) case. +pub fn parse_angle_bracketed(input: ParseStream) -> syn::Result> { + let mut items = Punctuated::new(); + + if !input.peek(Lt) { + return Ok(items); + } + + let _: Lt = input.parse()?; + + while !input.peek(Gt) { + items.push_value(input.parse()?); + + if input.peek(Gt) { + break; + } + + items.push_punct(input.parse()?); + } + + let _: Gt = input.parse()?; + + Ok(items) +} + +/// Emit a `< T, T, ... >` list, or nothing when the list is empty. The inverse +/// of [`parse_angle_bracketed`]. +pub fn to_tokens_angle_bracketed( + items: &Punctuated, + tokens: &mut TokenStream, +) { + if !items.is_empty() { + tokens.extend(quote! { < #items > }); + } +} diff --git a/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_args.rs b/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_args.rs index 3d32de1c..3110c8fc 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_args.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_args.rs @@ -3,12 +3,20 @@ use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::{Ident, Type, parse_quote}; -use crate::types::generics::GenericArguments; +use crate::traits::ToType; +use crate::types::ident::TypeArgs; +/// An identifier followed by an optional type-expression argument list, e.g. +/// `Foo`, `Foo`, `Foo<(A, B), C>`, or `Foo, C>`. +/// +/// For the path-headed counterpart (`path::to::Foo`), see +/// [`PathWithTypeArgs`]. +/// +/// [`PathWithTypeArgs`]: crate::types::ident::PathWithTypeArgs #[derive(Debug, Clone)] pub struct IdentWithTypeArgs { pub ident: Ident, - pub type_args: GenericArguments, + pub type_args: TypeArgs, } impl Parse for IdentWithTypeArgs { @@ -31,13 +39,19 @@ impl From for IdentWithTypeArgs { fn from(ident: Ident) -> Self { Self { ident, - type_args: GenericArguments::default(), + type_args: TypeArgs::default(), } } } +impl ToType for IdentWithTypeArgs { + fn to_type(&self) -> Type { + parse_quote!(#self) + } +} + impl From for Type { fn from(value: IdentWithTypeArgs) -> Self { - parse_quote!(#value) + value.to_type() } } diff --git a/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_generics.rs b/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_generics.rs index 9aa408f4..ecccf526 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_generics.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/ident_with_type_generics.rs @@ -3,12 +3,14 @@ use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::{Ident, Type, parse_quote}; -use crate::types::generics::TypeGenerics; +use crate::types::ident::TypeGenericParams; +/// An identifier followed by an optional definition-site generic parameter +/// list, e.g. `Foo`, `Foo`, or `Bar<'a, C>`. #[derive(Debug, Clone)] pub struct IdentWithTypeGenerics { pub ident: Ident, - pub type_generics: TypeGenerics, + pub type_generics: TypeGenericParams, } impl IdentWithTypeGenerics { @@ -21,7 +23,7 @@ impl From for IdentWithTypeGenerics { fn from(ident: Ident) -> Self { Self { ident, - type_generics: TypeGenerics::default(), + type_generics: TypeGenericParams::default(), } } } diff --git a/crates/macros/cgp-macro-core/src/types/ident/mod.rs b/crates/macros/cgp-macro-core/src/types/ident/mod.rs index c75490e4..f1b74865 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/mod.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/mod.rs @@ -1,5 +1,13 @@ +mod angle_bracketed; mod ident_with_type_args; mod ident_with_type_generics; +mod path_with_type_args; +mod type_arg; +mod type_generic_param; +pub use angle_bracketed::*; pub use ident_with_type_args::*; pub use ident_with_type_generics::*; +pub use path_with_type_args::*; +pub use type_arg::*; +pub use type_generic_param::*; diff --git a/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs b/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs new file mode 100644 index 00000000..d370ef1c --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs @@ -0,0 +1,135 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::{Error, Ident, Path, PathArguments, Type, parse_quote, parse2}; + +use crate::traits::ToType; +use crate::types::ident::{IdentWithTypeArgs, TypeArgs}; + +/// A full Rust path followed by an optional type-expression argument list, e.g. +/// `Foo`, `Foo`, `path::to::Foo`, or `path::to::Bar<(A, B), B>`. +/// +/// This generalizes [`IdentWithTypeArgs`] from a single identifier head to a +/// full [`syn::Path`] head. The motivation is that `syn::Path` keeps the final +/// generic arguments buried inside the last [`syn::PathSegment`], which is +/// awkward to read and rewrite. This type lifts those arguments out into a +/// separate [`TypeArgs`] field while keeping the remaining path in `path`, +/// applying the same restrictions as [`TypeArg`](crate::types::ident::TypeArg) +/// (no associated bindings or bounds). +/// +/// Only the final segment may carry generic arguments. Intermediate generics +/// (e.g. `path::to::Foo`) and parenthesized arguments (e.g. `Fn(A) -> B`) +/// are rejected. +#[derive(Debug, Clone)] +pub struct PathWithTypeArgs { + /// The full path with the final segment's arguments stripped, e.g. + /// `path::to::Foo` for an input of `path::to::Foo`. + pub path: Path, + /// The arguments lifted out of the final path segment, e.g. ``. + pub type_args: TypeArgs, +} + +impl PathWithTypeArgs { + /// The identifier of the final path segment, e.g. `Foo` in + /// `path::to::Foo`. + pub fn ident(&self) -> &Ident { + &self + .path + .segments + .last() + .expect("PathWithTypeArgs always wraps a non-empty syn::Path") + .ident + } +} + +impl Parse for PathWithTypeArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut path: Path = input.parse()?; + + let last_index = path.segments.len() - 1; + + // Generic arguments are only meaningful on the final segment for our + // use cases. Reject them on intermediate segments. + for (index, segment) in path.segments.iter().enumerate() { + if index != last_index && !segment.arguments.is_none() { + return Err(Error::new_spanned( + segment, + "generic arguments are only allowed on the final path segment", + )); + } + } + + let last_segment = path.segments.last_mut().unwrap(); + + let type_args = match &last_segment.arguments { + PathArguments::None => TypeArgs::default(), + PathArguments::AngleBracketed(arguments) => { + // Reject turbofish (`Foo::`); only the type-position form + // `Foo` is accepted, matching `IdentWithTypeArgs`. + if arguments.colon2_token.is_some() { + return Err(Error::new_spanned( + arguments, + "turbofish arguments (`Foo::`) are not allowed; use `Foo`", + )); + } + + // Re-parse the already-parsed `<...>` through `TypeArgs` so the + // argument-form restrictions (no associated bindings or bounds) + // live in exactly one place — `TypeArg`'s own parser — rather + // than being duplicated here against `syn::GenericArgument`. + // With the turbofish ruled out above, `arguments` re-emits as a + // plain `< .. >`, which is exactly what `TypeArgs` expects. + parse2::(arguments.to_token_stream())? + } + PathArguments::Parenthesized(arguments) => { + return Err(Error::new_spanned( + arguments, + "parenthesized generic arguments (`Fn(A) -> B`) are not allowed", + )); + } + }; + + // Keep `path` free of the final arguments so that `ToTokens` can + // reconstruct the original input as `path` followed by `type_args`. + last_segment.arguments = PathArguments::None; + + Ok(Self { path, type_args }) + } +} + +impl ToTokens for PathWithTypeArgs { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.path.to_tokens(tokens); + self.type_args.to_tokens(tokens); + } +} + +impl From for PathWithTypeArgs { + fn from(ident: Ident) -> Self { + Self { + path: Path::from(ident), + type_args: TypeArgs::default(), + } + } +} + +impl From for PathWithTypeArgs { + fn from(value: IdentWithTypeArgs) -> Self { + Self { + path: Path::from(value.ident), + type_args: value.type_args, + } + } +} + +impl ToType for PathWithTypeArgs { + fn to_type(&self) -> Type { + parse_quote!(#self) + } +} + +impl From for Type { + fn from(value: PathWithTypeArgs) -> Self { + value.to_type() + } +} diff --git a/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs new file mode 100644 index 00000000..aa0a161a --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs @@ -0,0 +1,123 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::token::{Brace, Comma}; +use syn::{Error, Expr, ExprBlock, ExprLit, Lifetime, Lit, Token, Type}; + +use crate::types::ident::{parse_angle_bracketed, to_tokens_angle_bracketed}; + +/// A single generic argument that can appear in a *type expression* position, +/// such as each of `'a`, `A`, `(A, B)`, and `Bar` inside +/// `Foo<'a, A, (A, B), Bar>`. +/// +/// This is a deliberately restricted version of [`syn::GenericArgument`]. The +/// `syn` type additionally accepts associated type bindings (`Item = T`), +/// associated const bindings (`N = 1`), and associated type bounds +/// (`Item: Clone`), none of which are valid in the plain type-argument +/// positions that CGP cares about. By modelling only the three valid forms, we +/// reject inputs like `Foo` at parse time. +#[derive(Debug, Clone)] +pub enum TypeArg { + /// A lifetime argument, e.g. the `'a` in `Foo<'a>`. + Lifetime(Lifetime), + /// A type argument, e.g. the `A`, `(A, B)`, or `Bar` in `Foo`. + Type(Type), + /// A const argument written as a literal or a braced block, e.g. the `3` + /// in `Foo<3>` or the `{ N }` in `Foo<{ N }>`. + /// + /// Note that, just like `syn`, a bare identifier const argument (such as + /// the `N` in `Foo`) is syntactically indistinguishable from a type and + /// is therefore parsed as [`TypeArg::Type`]. + Const(Expr), +} + +impl Parse for TypeArg { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(Lifetime) { + return Ok(Self::Lifetime(input.parse()?)); + } + + // Const arguments are only recognized when written as a literal or a + // braced block, mirroring `syn::GenericArgument`. A bare identifier is + // parsed as a `Type` instead. + if input.peek(Lit) { + let lit: Lit = input.parse()?; + return Ok(Self::Const(Expr::Lit(ExprLit { + attrs: Vec::new(), + lit, + }))); + } + + // A braced block must be parsed as an `ExprBlock` rather than a general + // `Expr`, because `Expr::parse` would greedily treat a following `>` as + // a comparison operator (e.g. parsing `{ N } >` as `{ N } > ...`). + if input.peek(Brace) { + let block: ExprBlock = input.parse()?; + return Ok(Self::Const(Expr::Block(block))); + } + + let ty: Type = input.parse()?; + + // After a complete type, the only valid continuation in an argument + // list is `,` or `>`. An `=` or `:` here indicates an associated + // binding or bound, which `syn::AngleBracketedGenericArguments` would + // silently accept but which is invalid in this position. + if input.peek(Token![=]) { + return Err(Error::new( + input.span(), + "associated bindings (`Name = ...`) are not allowed in type arguments", + )); + } + + if input.peek(Token![:]) { + return Err(Error::new( + input.span(), + "associated type bounds (`Name: ...`) are not allowed in type arguments", + )); + } + + Ok(Self::Type(ty)) + } +} + +impl ToTokens for TypeArg { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Lifetime(life) => life.to_tokens(tokens), + Self::Type(ty) => ty.to_tokens(tokens), + Self::Const(expr) => expr.to_tokens(tokens), + } + } +} + +/// The angle-bracketed argument list that follows an identifier or path in a +/// type expression, e.g. the `<'a, A, Bar>` in `Foo<'a, A, Bar>`. +/// +/// An empty list represents both the absence of any angle brackets (the bare +/// `Foo` case) and an explicit empty `Foo<>`; the two are not distinguished, and +/// an empty list always renders as nothing. +#[derive(Debug, Clone, Default)] +pub struct TypeArgs { + pub args: Punctuated, +} + +impl TypeArgs { + pub fn is_empty(&self) -> bool { + self.args.is_empty() + } +} + +impl Parse for TypeArgs { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + args: parse_angle_bracketed(input)?, + }) + } +} + +impl ToTokens for TypeArgs { + fn to_tokens(&self, tokens: &mut TokenStream) { + to_tokens_angle_bracketed(&self.args, tokens); + } +} diff --git a/crates/macros/cgp-macro-core/src/types/ident/type_generic_param.rs b/crates/macros/cgp-macro-core/src/types/ident/type_generic_param.rs new file mode 100644 index 00000000..8616bc77 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/type_generic_param.rs @@ -0,0 +1,177 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::token::{Colon, Comma, Const}; +use syn::{Error, Generics, Ident, Lifetime, Token, Type, parse_quote}; + +use crate::types::ident::{parse_angle_bracketed, to_tokens_angle_bracketed}; + +/// A single generic parameter that can appear at a *type definition* site, +/// such as each of `'a` and `C` inside `Bar<'a, C>`. +/// +/// This is a deliberately restricted version of [`syn::GenericParam`]. Unlike +/// the impl-generics used in `impl` blocks, definition-site parameters in CGP +/// are only ever simple, unconstrained parameters: a bare lifetime, a bare type +/// identifier, or a const parameter. In particular this rejects: +/// +/// - trait/lifetime bounds, e.g. `A: Clone` or `'a: 'b`, +/// - defaults, e.g. `A = B` or `const N: usize = 0`, +/// - composite forms, e.g. `(A, B)`. +/// +/// This complements (rather than replaces) [`TypeGenerics`], which detects +/// bounds by round-tripping a full `syn::Generics` through `split_for_impl`. +/// Modelling the valid forms directly here is clearer and catches more invalid +/// inputs (such as defaults) up front when *parsing tokens*; see +/// [`TypeGenericParams`] for guidance on which of the two to use. +/// +/// [`TypeGenerics`]: crate::types::generics::TypeGenerics +#[derive(Debug, Clone)] +pub enum TypeGenericParam { + /// A lifetime parameter, e.g. the `'a` in `Bar<'a>`. + Lifetime(Lifetime), + /// A type parameter, e.g. the `C` in `Bar`. + Type(Ident), + /// A const parameter, e.g. the `const N: usize` in `Bar`. + Const(Box), +} + +/// A const generic parameter at a definition site: the `const N: usize` in +/// `Bar`. Defaults (`= 0`) are deliberately not represented. +#[derive(Debug, Clone)] +pub struct ConstGenericParam { + pub const_token: Const, + pub ident: Ident, + pub colon: Colon, + pub ty: Type, +} + +impl Parse for TypeGenericParam { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(Lifetime) { + let life: Lifetime = input.parse()?; + + if input.peek(Token![:]) { + return Err(Error::new( + life.span(), + "lifetime bounds (`'a: 'b`) are not allowed in type generics", + )); + } + + return Ok(Self::Lifetime(life)); + } + + if input.peek(Token![const]) { + let const_token = input.parse()?; + let ident = input.parse()?; + let colon = input.parse()?; + let ty: Type = input.parse()?; + + if input.peek(Token![=]) { + return Err(Error::new( + input.span(), + "default const parameters (`const N: T = ...`) are not allowed in type generics", + )); + } + + return Ok(Self::Const(Box::new(ConstGenericParam { + const_token, + ident, + colon, + ty, + }))); + } + + let ident: Ident = input.parse()?; + + if input.peek(Token![:]) { + return Err(Error::new( + ident.span(), + "trait bounds (`A: Clone`) are not allowed in type generics", + )); + } + + if input.peek(Token![=]) { + return Err(Error::new( + ident.span(), + "default type parameters (`A = B`) are not allowed in type generics", + )); + } + + Ok(Self::Type(ident)) + } +} + +impl ToTokens for TypeGenericParam { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Lifetime(life) => life.to_tokens(tokens), + Self::Type(ident) => ident.to_tokens(tokens), + Self::Const(param) => { + param.const_token.to_tokens(tokens); + param.ident.to_tokens(tokens); + param.colon.to_tokens(tokens); + param.ty.to_tokens(tokens); + } + } + } +} + +/// The angle-bracketed parameter list at a type definition site, e.g. the +/// `<'a, C>` in `Bar<'a, C>`. +/// +/// # `TypeGenericParams` vs [`TypeGenerics`] +/// +/// Both model a definition-site generic list, but they are different tools: +/// +/// - Reach for `TypeGenericParams` when **parsing tokens** where you want the +/// restrictions enforced strictly and the parameters classified by kind. It +/// is a hand-written parser that rejects bounds and defaults up front and +/// exposes each parameter as a [`TypeGenericParam`] variant. +/// - Reach for [`TypeGenerics`] when adapting an **already-parsed +/// [`syn::Generics`]** (e.g. off an `ItemTrait`/`ItemStruct`). It is a thin +/// newtype that `Deref`s to `syn::Generics`, so `split_for_impl()` and the +/// usual `syn` manipulation are available, and its `TryFrom<&Generics>` +/// normalizes through `split_for_impl` (which, notably, collapses a +/// `const N: T` parameter down to a bare type-like `N`). +/// +/// They are intentionally not merged: the normalization behavior above is +/// load-bearing for some callers, so a faithful conversion into the strict +/// `TypeGenericParam` model would change behavior around const generics. +/// +/// [`TypeGenerics`]: crate::types::generics::TypeGenerics +/// +/// Both the absence of angle brackets and an explicit empty `<>` are +/// represented as an empty [`Punctuated`]. An empty list renders as nothing, +/// so a parsed `<>` round-trips back to no angle brackets. +#[derive(Debug, Clone, Default)] +pub struct TypeGenericParams { + pub params: Punctuated, +} + +impl TypeGenericParams { + pub fn is_empty(&self) -> bool { + self.params.is_empty() + } + + /// Lower these parameters into a plain [`syn::Generics`]. This is handy for + /// downstream code that needs to feed the parameters into constructs (such + /// as struct definitions) that are expressed in terms of `syn::Generics`. + pub fn to_generics(&self) -> Generics { + parse_quote!( #self ) + } +} + +impl Parse for TypeGenericParams { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + params: parse_angle_bracketed(input)?, + }) + } +} + +impl ToTokens for TypeGenericParams { + fn to_tokens(&self, tokens: &mut TokenStream) { + to_tokens_angle_bracketed(&self.params, tokens); + } +} diff --git a/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs b/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs index 836fe8ec..b3eaf2f3 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs @@ -2,11 +2,11 @@ use syn::{Generics, Ident, Type}; use crate::parse_internal; use crate::types::delegate_component::{EvalForEntry, EvaluatedForEntry}; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; #[derive(Debug, Clone)] pub struct InheritNamespaceStatement { - pub namespace: IdentWithTypeArgs, + pub namespace: PathWithTypeArgs, pub local_table_ident: Ident, } @@ -17,7 +17,7 @@ impl EvalForEntry for InheritNamespaceStatement { let mut namespace_constraint = self.namespace.clone(); namespace_constraint .type_args - .make_args() + .args .push(parse_internal!(#local_table_ident)); let mut generics = Generics::default(); diff --git a/crates/macros/cgp-macro-core/src/types/namespace/table.rs b/crates/macros/cgp-macro-core/src/types/namespace/table.rs index 54938e9e..534dd652 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/table.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/table.rs @@ -8,7 +8,7 @@ use crate::types::delegate_component::{ DelegateEntries, EvalDelegateEntries, EvalDelegateEntry, EvalForEntry, }; use crate::types::generics::ImplGenerics; -use crate::types::ident::{IdentWithTypeArgs, IdentWithTypeGenerics}; +use crate::types::ident::{IdentWithTypeGenerics, PathWithTypeArgs}; use crate::types::keyword::Keyword; use crate::types::keywords::New; use crate::types::namespace::{EvaluatedNamespaceTable, InheritNamespaceStatement}; @@ -17,7 +17,7 @@ pub struct NamespaceTable { pub impl_generics: ImplGenerics, pub new: Option>, pub namespace: IdentWithTypeGenerics, - pub parent_namespace: Option<(Colon, IdentWithTypeArgs)>, + pub parent_namespace: Option<(Colon, PathWithTypeArgs)>, pub entries: DelegateEntries, } @@ -109,7 +109,7 @@ impl NamespaceTable { if self.new.is_none() { return Err(Error::new( - parent_namespace.ident.span(), + parent_namespace.ident().span(), "parent namespace can only be specified with `new` namespaces", )); } diff --git a/crates/macros/cgp-macro-core/src/types/provider_impl.rs b/crates/macros/cgp-macro-core/src/types/provider_impl.rs index a6b7cff2..81b81051 100644 --- a/crates/macros/cgp-macro-core/src/types/provider_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/provider_impl.rs @@ -9,7 +9,7 @@ use syn::{Error, ItemImpl, Path, Type}; use crate::exports::IsProviderFor; use crate::functions::parse_internal; use crate::types::cgp_provider::ProviderImplArgs; -use crate::types::ident::IdentWithTypeArgs; +use crate::types::ident::PathWithTypeArgs; use crate::visitors::replace_provider_in_generics; pub fn derive_is_provider_for( @@ -59,12 +59,11 @@ impl ItemProviderImpl { Error::new(item_impl.span(), "provider impl should contain trait path") })?; - let IdentWithTypeArgs { - ident: provider_ident, - type_args: provider_generics, - } = parse_internal(provider_path.to_token_stream())?; + let provider: PathWithTypeArgs = parse_internal(provider_path.to_token_stream())?; + let provider_ident = provider.ident().clone(); + let provider_generics = &provider.type_args; - let impl_args = ProviderImplArgs::from_generic_args(&provider_generics)?; + let impl_args = ProviderImplArgs::from_generic_args(provider_generics)?; let context_type = &impl_args.context_type; let is_provider_path: Path = diff --git a/crates/macros/cgp-macro-lib/src/parse/component_spec.rs b/crates/macros/cgp-macro-lib/src/parse/component_spec.rs deleted file mode 100644 index 05c840c3..00000000 --- a/crates/macros/cgp-macro-lib/src/parse/component_spec.rs +++ /dev/null @@ -1,106 +0,0 @@ -use alloc::format; -use std::collections::BTreeMap; - -use cgp_macro_core::types::cgp_component::DeriveDelegateAttributes; -use cgp_macro_core::types::ident::IdentWithTypeGenerics; -use proc_macro2::{Span, TokenStream}; -use syn::parse::{End, Parse, ParseStream}; -use syn::{Error, Ident, parse2}; - -use crate::parse::Entries; - -pub struct CgpComponentArgs { - pub provider_ident: Ident, - pub context_ident: Ident, - pub component_name: IdentWithTypeGenerics, - pub derive_delegate_attributes: DeriveDelegateAttributes, -} - -static VALID_KEYS: [&str; 4] = ["context", "provider", "name", "derive_delegate"]; - -impl Parse for CgpComponentArgs { - fn parse(input: ParseStream) -> syn::Result { - if input.peek2(End) { - let provider_name: Ident = input.parse()?; - - let context_type = Ident::new("__Context__", Span::call_site()); - - let component_name = - Ident::new(&format!("{provider_name}Component"), provider_name.span()); - - Ok(Self { - provider_ident: provider_name, - context_ident: context_type, - component_name: component_name.into(), - derive_delegate_attributes: Default::default(), - }) - } else { - let Entries { entries } = input.parse()?; - Self::from_entries(&entries) - } - } -} - -impl CgpComponentArgs { - pub fn validate_entries(entries: &BTreeMap) -> syn::Result<()> { - for key in entries.keys() { - if !VALID_KEYS.iter().any(|valid| valid == key) { - return Err(syn::Error::new( - Span::call_site(), - format!( - r#"invalid key in component spec: {key}. the following keys are valid: "context", "provider", "name"."# - ), - )); - } - } - - Ok(()) - } - - pub fn from_entries(entries: &BTreeMap) -> syn::Result { - Self::validate_entries(entries)?; - - let context_type: Ident = { - let raw_context_type = entries.get("context"); - - if let Some(context_type) = raw_context_type { - syn::parse2(context_type.clone())? - } else { - Ident::new("__Context__", Span::call_site()) - } - }; - - let provider_name: Ident = { - let raw_provider_name = entries - .get("provider") - .ok_or_else(|| Error::new(Span::call_site(), "expect provider name to be given"))?; - - syn::parse2(raw_provider_name.clone())? - }; - - let component_name = { - let raw_component_name = entries.get("name"); - - if let Some(raw_component_name) = raw_component_name { - parse2(raw_component_name.clone())? - } else { - IdentWithTypeGenerics::from(Ident::new( - &format!("{provider_name}Component"), - provider_name.span(), - )) - } - }; - - let derive_delegate_attributes = match entries.get("derive_delegate") { - Some(entry) => parse2(entry.clone())?, - None => Default::default(), - }; - - Ok(CgpComponentArgs { - component_name, - provider_ident: provider_name, - context_ident: context_type, - derive_delegate_attributes, - }) - } -} diff --git a/crates/tests/cgp-macro-tests/Cargo.toml b/crates/tests/cgp-macro-tests/Cargo.toml new file mode 100644 index 00000000..58d584bc --- /dev/null +++ b/crates/tests/cgp-macro-tests/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cgp-macro-tests" +version = "0.7.0" +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +rust-version = { workspace = true } +keywords = { workspace = true } + +[dependencies] +cgp = { workspace = true } +cgp-macro-test-util = { workspace = true } + +[dev-dependencies] +cgp-macro-core = { workspace = true } +syn = { version = "2.0.95" } +quote = { version = "1.0.38" } +proc-macro2 = { version = "1.0.92" } diff --git a/crates/tests/cgp-macro-tests/src/lib.rs b/crates/tests/cgp-macro-tests/src/lib.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/tests/cgp-macro-tests/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/tests/cgp-macro-tests/tests/ident_with_type_params.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params.rs new file mode 100644 index 00000000..b8a9f0d1 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/ident_with_type_params.rs @@ -0,0 +1 @@ +pub mod ident_with_type_params_tests; diff --git a/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/mod.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/mod.rs new file mode 100644 index 00000000..0a270c04 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/mod.rs @@ -0,0 +1,50 @@ +pub mod new_ident_with_type_args; +pub mod new_ident_with_type_generics; +pub mod path_with_type_args; + +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::Parse; +use syn::parse2; + +/// Assert that `tokens` parse successfully as `T`. +#[track_caller] +pub fn assert_parses(tokens: TokenStream) { + if let Err(err) = parse2::(tokens.clone()) { + panic!( + "expected `{tokens}` to parse as `{}`, but parsing failed: {err}", + core::any::type_name::(), + ); + } +} + +/// Assert that `tokens` are rejected (fail to parse) as `T`. +#[track_caller] +pub fn assert_rejects(tokens: TokenStream) { + if parse2::(tokens.clone()).is_ok() { + panic!( + "expected `{tokens}` to be rejected as `{}`, but it parsed successfully", + core::any::type_name::(), + ); + } +} + +/// Assert that re-emitting the parsed value and parsing it again yields the +/// same token stream. This verifies that `parse` then `to_tokens` is a stable +/// round trip, while ignoring purely cosmetic spacing differences against the +/// original source. +#[track_caller] +pub fn assert_idempotent(tokens: TokenStream) { + let first: T = + parse2(tokens.clone()).unwrap_or_else(|e| panic!("parse failed for `{tokens}`: {e}")); + let emitted = first.to_token_stream(); + + let second: T = parse2(emitted.clone()) + .unwrap_or_else(|e| panic!("re-parse failed for `{emitted}` (from `{tokens}`): {e}")); + + assert_eq!( + emitted.to_string(), + second.to_token_stream().to_string(), + "emission is not idempotent for `{tokens}`", + ); +} diff --git a/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs new file mode 100644 index 00000000..0bb2ca6b --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs @@ -0,0 +1,133 @@ +//! Corner cases for `IdentWithTypeArgs` — an identifier followed by an +//! optional *type-expression* argument list, e.g. `Foo`. + +use cgp_macro_core::types::ident::{IdentWithTypeArgs, TypeArg}; +use quote::quote; +use syn::parse2; + +use super::{assert_idempotent, assert_parses, assert_rejects}; + +type Subject = IdentWithTypeArgs; + +#[test] +fn accepts_bare_ident() { + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_empty_argument_list() { + // An explicit empty `<>` is allowed and parses to an empty argument list, + // indistinguishable from no brackets at all. + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_type_arguments() { + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_composite_type_arguments() { + // The key distinguishing feature versus the definition-site generics: + // arguments may be arbitrary types, not just simple identifiers. + assert_parses::(quote!(Foo<(A, B), C>)); + assert_parses::(quote!(Foo, C>)); + assert_parses::(quote!(Foo>>)); + assert_parses::(quote!(Foo<[A; 4]>)); + assert_parses::(quote!(Foo<&'a A>)); + assert_parses::(quote!(Foo B>)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo>)); +} + +#[test] +fn accepts_lifetime_arguments() { + assert_parses::(quote!(Foo<'a>)); + assert_parses::(quote!(Foo<'a, A>)); + assert_parses::(quote!(Foo<'a, 'b, A>)); +} + +#[test] +fn accepts_const_arguments() { + // Const arguments are recognized when written as a literal or braced block. + assert_parses::(quote!(Foo<3>)); + assert_parses::(quote!(Foo<{ N }>)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); +} + +#[test] +fn rejects_associated_type_binding() { + // `syn::AngleBracketedGenericArguments` would accept these, but they are + // not valid in a plain type-argument position. + assert_rejects::(quote!(Foo)); + assert_rejects::(quote!(Foo)); +} + +#[test] +fn rejects_associated_const_binding() { + assert_rejects::(quote!(Foo)); +} + +#[test] +fn rejects_associated_type_bound() { + assert_rejects::(quote!(Foo)); + assert_rejects::(quote!(Foo)); +} + +#[test] +fn rejects_path_head() { + // The head must be a single identifier; use `PathWithTypeArgs` for paths. + assert_rejects::(quote!(path::to::Foo)); + assert_rejects::(quote!(path::to::Foo)); +} + +#[test] +fn rejects_turbofish() { + assert_rejects::(quote!(Foo::)); +} + +#[test] +fn rejects_unterminated_arguments() { + assert_rejects::(quote!(Foo < A)); + assert_rejects::(quote!(Foo < A,)); +} + +#[test] +fn classifies_each_argument_form() { + let parsed: Subject = parse2(quote!(Foo<'a, A, (A, B), Bar, 3, { N }>)).unwrap(); + + let args = &parsed.type_args.args; + + let kinds: Vec<&str> = args + .iter() + .map(|arg| match arg { + TypeArg::Lifetime(_) => "lifetime", + TypeArg::Type(_) => "type", + TypeArg::Const(_) => "const", + }) + .collect(); + + assert_eq!( + kinds, + ["lifetime", "type", "type", "type", "const", "const"], + ); +} + +#[test] +fn bare_ident_has_no_arguments() { + let parsed: Subject = parse2(quote!(Foo)).unwrap(); + assert!(parsed.type_args.args.is_empty()); + assert!(parsed.type_args.is_empty()); +} + +#[test] +fn round_trips() { + assert_idempotent::(quote!(Foo)); + assert_idempotent::(quote!(Foo)); + assert_idempotent::(quote!(Foo<(A, B), Bar>)); + assert_idempotent::(quote!(Foo<'a, A, 3>)); +} diff --git a/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs new file mode 100644 index 00000000..4a0b25a8 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs @@ -0,0 +1,116 @@ +//! Corner cases for `IdentWithTypeGenerics` — an identifier followed by an +//! optional *definition-site* generic parameter list, e.g. `Foo` or +//! `Bar<'a, C>`. + +use cgp_macro_core::types::ident::{IdentWithTypeGenerics, TypeGenericParam}; +use quote::quote; +use syn::parse2; + +use super::{assert_idempotent, assert_parses, assert_rejects}; + +type Subject = IdentWithTypeGenerics; + +#[test] +fn accepts_bare_ident() { + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_empty_parameter_list() { + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_simple_type_parameters() { + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_lifetime_parameters() { + assert_parses::(quote!(Bar<'a>)); + assert_parses::(quote!(Bar<'a, C>)); + assert_parses::(quote!(Bar<'a, 'b, C>)); +} + +#[test] +fn accepts_const_parameters() { + assert_parses::(quote!(Bar)); + assert_parses::(quote!(Bar)); +} + +#[test] +fn rejects_trait_bounds() { + assert_rejects::(quote!(Foo)); + assert_rejects::(quote!(Foo)); +} + +#[test] +fn rejects_lifetime_bounds() { + assert_rejects::(quote!(Foo<'a: 'b>)); +} + +#[test] +fn rejects_defaults() { + assert_rejects::(quote!(Foo)); + assert_rejects::(quote!(Bar)); +} + +#[test] +fn rejects_composite_parameters() { + // Definition-site parameters must be simple; composite forms that are + // valid as *arguments* are not valid as *parameters*. + assert_rejects::(quote!(Foo<(A, B)>)); + assert_rejects::(quote!(Foo>)); + assert_rejects::(quote!(Foo<&'a A>)); +} + +#[test] +fn rejects_path_head() { + assert_rejects::(quote!(path::to::Foo)); +} + +#[test] +fn classifies_each_parameter_form() { + let parsed: Subject = parse2(quote!(Bar<'a, C, const N: usize>)).unwrap(); + + let params = &parsed.type_generics.params; + + let kinds: Vec<&str> = params + .iter() + .map(|param| match param { + TypeGenericParam::Lifetime(_) => "lifetime", + TypeGenericParam::Type(_) => "type", + TypeGenericParam::Const(_) => "const", + }) + .collect(); + + assert_eq!(kinds, ["lifetime", "type", "const"]); +} + +#[test] +fn lowers_to_syn_generics() { + let parsed: Subject = parse2(quote!(Bar<'a, C, const N: usize>)).unwrap(); + let generics = parsed.type_generics.to_generics(); + + assert_eq!(generics.params.len(), 3); + assert!(matches!(generics.params[0], syn::GenericParam::Lifetime(_))); + assert!(matches!(generics.params[1], syn::GenericParam::Type(_))); + assert!(matches!(generics.params[2], syn::GenericParam::Const(_))); +} + +#[test] +fn empty_generics_lower_to_empty_syn_generics() { + let parsed: Subject = parse2(quote!(Foo)).unwrap(); + let generics = parsed.type_generics.to_generics(); + assert!(generics.params.is_empty()); +} + +#[test] +fn round_trips() { + assert_idempotent::(quote!(Foo)); + assert_idempotent::(quote!(Foo)); + assert_idempotent::(quote!(Bar<'a, C>)); + assert_idempotent::(quote!(Bar<'a, C, const N: usize>)); +} diff --git a/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/path_with_type_args.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/path_with_type_args.rs new file mode 100644 index 00000000..c6431011 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/path_with_type_args.rs @@ -0,0 +1,101 @@ +//! Corner cases for `PathWithTypeArgs` — a full Rust path followed by an +//! optional type-expression argument list, e.g. `path::to::Foo`. + +use cgp_macro_core::types::ident::{PathWithTypeArgs, TypeArg}; +use quote::quote; +use syn::parse2; + +use super::{assert_idempotent, assert_parses, assert_rejects}; + +type Subject = PathWithTypeArgs; + +#[test] +fn accepts_single_segment() { + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_multi_segment_paths() { + assert_parses::(quote!(path::to::Foo)); + assert_parses::(quote!(path::to::Foo)); + assert_parses::(quote!(path::to::Bar<(A, B), B>)); + assert_parses::(quote!(crate::module::Foo)); + assert_parses::(quote!(self::Foo)); +} + +#[test] +fn accepts_leading_colon() { + assert_parses::(quote!(::path::to::Foo)); + assert_parses::(quote!(::path::to::Foo<'a, A>)); +} + +#[test] +fn accepts_same_argument_forms_as_ident_args() { + assert_parses::(quote!(path::to::Foo<'a, A, (A, B), Bar, 3>)); +} + +#[test] +fn rejects_intermediate_segment_generics() { + // Generic arguments are only meaningful on the final segment. + assert_rejects::(quote!(path::to::Foo)); + assert_rejects::(quote!(path::to::Foo)); +} + +#[test] +fn rejects_turbofish() { + assert_rejects::(quote!(path::to::Foo::)); + assert_rejects::(quote!(Foo::)); +} + +#[test] +fn rejects_associated_bindings_and_bounds() { + assert_rejects::(quote!(path::to::Foo)); + assert_rejects::(quote!(path::to::Foo)); + assert_rejects::(quote!(path::to::Foo)); +} + +#[test] +fn rejects_parenthesized_arguments() { + // `Fn(A) -> B` style parenthesized arguments are not allowed. + assert_rejects::(quote!(path::to::Fn(A) -> B)); +} + +#[test] +fn exposes_final_segment_ident() { + let parsed: Subject = parse2(quote!(path::to::Foo)).unwrap(); + assert_eq!(parsed.ident().to_string(), "Foo"); + + let single: Subject = parse2(quote!(Foo)).unwrap(); + assert_eq!(single.ident().to_string(), "Foo"); +} + +#[test] +fn strips_arguments_from_stored_path() { + let parsed: Subject = parse2(quote!(path::to::Foo)).unwrap(); + + // The arguments are lifted out into `type_args`, leaving the path itself + // free of the final-segment arguments. + let last = parsed.path.segments.last().unwrap(); + assert!(last.arguments.is_none()); + + let args = &parsed.type_args.args; + assert_eq!(args.len(), 2); + assert!(matches!(args[0], TypeArg::Type(_))); +} + +#[test] +fn single_segment_path_has_no_args_for_bare_ident() { + let parsed: Subject = parse2(quote!(path::to::Foo)).unwrap(); + assert!(parsed.type_args.args.is_empty()); + assert_eq!(parsed.path.segments.len(), 3); +} + +#[test] +fn round_trips() { + assert_idempotent::(quote!(Foo)); + assert_idempotent::(quote!(path::to::Foo)); + assert_idempotent::(quote!(path::to::Foo)); + assert_idempotent::(quote!(path::to::Bar<(A, B), Baz>)); + assert_idempotent::(quote!(::path::to::Foo<'a, A>)); +} diff --git a/crates/tests/cgp-tests/Cargo.toml b/crates/tests/cgp-tests/Cargo.toml index 1575c261..f7d4ca57 100644 --- a/crates/tests/cgp-tests/Cargo.toml +++ b/crates/tests/cgp-tests/Cargo.toml @@ -13,3 +13,9 @@ cgp = { workspace = true } cgp-macro-test-util = { workspace = true } insta = { version = "1.48.0" } futures = { version = "0.3.31" } + +[dev-dependencies] +cgp-macro-core = { workspace = true } +syn = { version = "2.0.95", features = [ "full", "extra-traits" ] } +quote = { version = "1.0.38" } +proc-macro2 = { version = "1.0.92" }