From e3a964531060b7bd352ef6831bc744f74b93e239 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 10:32:05 +0200 Subject: [PATCH 01/15] AI draft new ident with params types --- .../cgp-macro-core/src/types/ident/README.md | 0 .../src/types/ident/draft_tests.rs | 105 ++++++++++ .../cgp-macro-core/src/types/ident/mod.rs | 11 ++ .../types/ident/new_ident_with_type_args.rs | 62 ++++++ .../ident/new_ident_with_type_generics.rs | 55 ++++++ .../src/types/ident/path_with_type_args.rs | 136 +++++++++++++ .../src/types/ident/type_arg.rs | 181 ++++++++++++++++++ .../src/types/ident/type_generic_param.rs | 181 ++++++++++++++++++ .../cgp-tests/tests/ident_with_type_params.rs | 1 + .../tests/ident_with_type_params_tests/mod.rs | 1 + 10 files changed, 733 insertions(+) create mode 100644 crates/macros/cgp-macro-core/src/types/ident/README.md create mode 100644 crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs create mode 100644 crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_args.rs create mode 100644 crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs create mode 100644 crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs create mode 100644 crates/macros/cgp-macro-core/src/types/ident/type_arg.rs create mode 100644 crates/macros/cgp-macro-core/src/types/ident/type_generic_param.rs create mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params.rs create mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs diff --git a/crates/macros/cgp-macro-core/src/types/ident/README.md b/crates/macros/cgp-macro-core/src/types/ident/README.md new file mode 100644 index 00000000..e69de29b diff --git a/crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs b/crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs new file mode 100644 index 00000000..c7b56b11 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs @@ -0,0 +1,105 @@ +#![cfg(test)] + +use quote::quote; +use syn::parse2; + +use crate::types::ident::{NewIdentWithTypeArgs, NewIdentWithTypeGenerics, PathWithTypeArgs}; + +fn args_ok(ts: proc_macro2::TokenStream) { + parse2::(ts.clone()) + .unwrap_or_else(|e| panic!("expected ok for `{ts}`: {e}")); +} +fn args_err(ts: proc_macro2::TokenStream) { + assert!( + parse2::(ts.clone()).is_err(), + "expected err for `{ts}`" + ); +} +fn gen_ok(ts: proc_macro2::TokenStream) { + parse2::(ts.clone()) + .unwrap_or_else(|e| panic!("expected ok for `{ts}`: {e}")); +} +fn gen_err(ts: proc_macro2::TokenStream) { + assert!( + parse2::(ts.clone()).is_err(), + "expected err for `{ts}`" + ); +} +fn path_ok(ts: proc_macro2::TokenStream) { + parse2::(ts.clone()) + .unwrap_or_else(|e| panic!("expected ok for `{ts}`: {e}")); +} +fn path_err(ts: proc_macro2::TokenStream) { + assert!( + parse2::(ts.clone()).is_err(), + "expected err for `{ts}`" + ); +} + +// Compare re-emitted tokens by re-parsing both into `syn::Type`, so that +// purely cosmetic spacing differences (e.g. `> >` vs `>>`) are ignored. +fn roundtrip_args(ts: proc_macro2::TokenStream) { + let parsed: NewIdentWithTypeArgs = parse2(ts.clone()).unwrap(); + let a: syn::Type = parse2(quote!(#parsed)).unwrap(); + let b: syn::Type = parse2(ts).unwrap(); + assert_eq!(a, b); +} +fn roundtrip_path(ts: proc_macro2::TokenStream) { + let parsed: PathWithTypeArgs = parse2(ts.clone()).unwrap(); + let a: syn::Type = parse2(quote!(#parsed)).unwrap(); + let b: syn::Type = parse2(ts).unwrap(); + assert_eq!(a, b); +} + +#[test] +fn type_args() { + args_ok(quote!(Foo)); + args_ok(quote!(Foo)); + args_ok(quote!(Foo<(A, B), C>)); + args_ok(quote!(Foo, C>)); + args_ok(quote!(Foo<'a, A>)); + args_ok(quote!(Foo<3>)); + args_ok(quote!(Foo<{ N }>)); + + args_err(quote!(Foo)); + args_err(quote!(Foo)); + args_err(quote!(Foo)); +} + +#[test] +fn type_generics() { + gen_ok(quote!(Foo)); + gen_ok(quote!(Foo)); + gen_ok(quote!(Bar<'a, C>)); + gen_ok(quote!(Bar)); + + gen_err(quote!(Foo)); + gen_err(quote!(Foo)); + gen_err(quote!(Foo<(A, B)>)); + gen_err(quote!(Foo>)); + gen_err(quote!(Bar)); +} + +#[test] +fn paths() { + path_ok(quote!(Foo)); + path_ok(quote!(Foo)); + path_ok(quote!(path::to::Foo)); + path_ok(quote!(path::to::Foo)); + path_ok(quote!(path::to::Bar<(A, B), B>)); + + path_err(quote!(path::to::Foo)); + path_err(quote!(path::to::Foo)); + path_err(quote!(path::to::Foo::)); + + let parsed: PathWithTypeArgs = parse2(quote!(path::to::Foo)).unwrap(); + assert_eq!(parsed.ident().to_string(), "Foo"); +} + +#[test] +fn roundtrips() { + roundtrip_args(quote!(Foo)); + roundtrip_args(quote!(Foo<(A, B), Bar>)); + roundtrip_path(quote!(path::to::Foo)); + roundtrip_path(quote!(::path::to::Foo<'a, A>)); +} 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..898efe80 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,16 @@ +mod draft_tests; mod ident_with_type_args; mod ident_with_type_generics; +mod new_ident_with_type_args; +mod new_ident_with_type_generics; +mod path_with_type_args; +mod type_arg; +mod type_generic_param; pub use ident_with_type_args::*; pub use ident_with_type_generics::*; +pub use new_ident_with_type_args::*; +pub use new_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/new_ident_with_type_args.rs b/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_args.rs new file mode 100644 index 00000000..e3525b4a --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_args.rs @@ -0,0 +1,62 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Type, parse_quote}; + +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>`. +/// +/// This is the intended replacement for `IdentWithTypeArgs`. The difference is +/// that the argument list is modelled by [`TypeArgs`] rather than +/// `syn::AngleBracketedGenericArguments`, so invalid forms such as +/// `Foo` are rejected at parse time. +/// +/// For the path-headed counterpart (`path::to::Foo`), see +/// [`PathWithTypeArgs`]. +/// +/// [`PathWithTypeArgs`]: crate::types::ident::PathWithTypeArgs +#[derive(Debug, Clone)] +pub struct NewIdentWithTypeArgs { + pub ident: Ident, + pub type_args: TypeArgs, +} + +impl Parse for NewIdentWithTypeArgs { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.parse()?; + let type_args = input.parse()?; + + Ok(Self { ident, type_args }) + } +} + +impl ToTokens for NewIdentWithTypeArgs { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.ident.to_tokens(tokens); + self.type_args.to_tokens(tokens); + } +} + +impl From for NewIdentWithTypeArgs { + fn from(ident: Ident) -> Self { + Self { + ident, + type_args: TypeArgs::default(), + } + } +} + +impl ToType for NewIdentWithTypeArgs { + fn to_type(&self) -> Type { + parse_quote!(#self) + } +} + +impl From for Type { + fn from(value: NewIdentWithTypeArgs) -> Self { + parse_quote!(#value) + } +} diff --git a/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs b/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs new file mode 100644 index 00000000..8eb96b01 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs @@ -0,0 +1,55 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Type, parse_quote}; + +use crate::types::ident::TypeGenericParams; + +/// An identifier followed by an optional definition-site generic parameter +/// list, e.g. `Foo`, `Foo`, or `Bar<'a, C>`. +/// +/// This is the intended replacement for `IdentWithTypeGenerics`. The difference +/// is that the parameter list is modelled by [`TypeGenericParams`] rather than +/// `syn::Generics`, so only simple, unconstrained parameters are accepted. +/// Invalid forms such as `Foo` (bounds) and `Foo` (defaults) +/// are rejected at parse time, without the `split_for_impl` round-trip hack +/// used by the original `TypeGenerics`. +#[derive(Debug, Clone)] +pub struct NewIdentWithTypeGenerics { + pub ident: Ident, + pub type_generics: TypeGenericParams, +} + +impl NewIdentWithTypeGenerics { + pub fn to_type(&self) -> Type { + parse_quote!(#self) + } +} + +impl From for NewIdentWithTypeGenerics { + fn from(ident: Ident) -> Self { + Self { + ident, + type_generics: TypeGenericParams::default(), + } + } +} + +impl Parse for NewIdentWithTypeGenerics { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.parse()?; + let type_generics = input.parse()?; + + Ok(Self { + ident, + type_generics, + }) + } +} + +impl ToTokens for NewIdentWithTypeGenerics { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.ident.to_tokens(tokens); + self.type_generics.to_tokens(tokens); + } +} 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..cb1ffd2e --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs @@ -0,0 +1,136 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Error, Ident, Path, PathArguments, Type, parse_quote}; + +use crate::traits::ToType; +use crate::types::ident::{NewIdentWithTypeArgs, TypeArg, 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 [`NewIdentWithTypeArgs`] 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`] (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 { + // A parsed `syn::Path` always has at least one segment. + &self.path.segments.last().unwrap().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 { args: None }, + PathArguments::AngleBracketed(arguments) => { + // Reject turbofish (`Foo::`); only the type-position form + // `Foo` is accepted, matching `NewIdentWithTypeArgs`. + if arguments.colon2_token.is_some() { + return Err(Error::new_spanned( + arguments, + "turbofish arguments (`Foo::`) are not allowed; use `Foo`", + )); + } + + let mut args = Punctuated::new(); + + for pair in arguments.args.pairs() { + let (arg, punct) = pair.into_tuple(); + args.push_value(TypeArg::from_generic_argument(arg)?); + if let Some(comma) = punct { + args.push_punct(*comma); + } + } + + TypeArgs { args: Some(args) } + } + 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: NewIdentWithTypeArgs) -> 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 { + parse_quote!(#value) + } +} 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..bf574b2f --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs @@ -0,0 +1,181 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::token::{Brace, Comma, Gt, Lt}; +use syn::{Error, Expr, ExprBlock, ExprLit, GenericArgument, Lifetime, Lit, Token, Type}; + +/// 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 TypeArg { + /// Convert a [`syn::GenericArgument`] into a [`TypeArg`], rejecting the + /// associated-binding and bound forms that are not valid in type-argument + /// positions. + /// + /// This is useful when post-processing a value that `syn` has already + /// parsed into a [`syn::Path`] (see [`PathWithTypeArgs`]). + /// + /// [`PathWithTypeArgs`]: crate::types::ident::PathWithTypeArgs + pub fn from_generic_argument(arg: &GenericArgument) -> syn::Result { + match arg { + GenericArgument::Lifetime(life) => Ok(Self::Lifetime(life.clone())), + GenericArgument::Type(ty) => Ok(Self::Type(ty.clone())), + GenericArgument::Const(expr) => Ok(Self::Const(expr.clone())), + GenericArgument::AssocType(_) | GenericArgument::AssocConst(_) => { + Err(Error::new_spanned( + arg, + "associated bindings (`Name = ...`) are not allowed in type arguments", + )) + } + GenericArgument::Constraint(_) => Err(Error::new_spanned( + arg, + "associated type bounds (`Name: ...`) are not allowed in type arguments", + )), + _ => Err(Error::new_spanned(arg, "unsupported generic argument")), + } + } +} + +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 optional 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>`. +/// +/// `None` represents the absence of any angle brackets (the bare `Foo` case), +/// whereas `Some(empty)` represents an explicit empty `Foo<>`. This mirrors the +/// behaviour of the existing `GenericArguments` type so the two can be used +/// interchangeably during migration. +#[derive(Debug, Clone, Default)] +pub struct TypeArgs { + pub args: Option>, +} + +impl TypeArgs { + /// Get a mutable reference to the underlying argument list, inserting an + /// empty `<>` list if none is present yet. This matches the + /// `GenericArguments::make_args` API to ease migration. + pub fn make_args(&mut self) -> &mut Punctuated { + self.args.get_or_insert_with(Punctuated::new) + } + + pub fn is_empty(&self) -> bool { + match &self.args { + Some(args) => args.is_empty(), + None => true, + } + } +} + +impl Parse for TypeArgs { + fn parse(input: ParseStream) -> syn::Result { + if !input.peek(Lt) { + return Ok(Self { args: None }); + } + + let _: Lt = input.parse()?; + + let mut args = Punctuated::new(); + + while !input.peek(Gt) { + args.push_value(input.parse()?); + + if input.peek(Gt) { + break; + } + + args.push_punct(input.parse()?); + } + + let _: Gt = input.parse()?; + + Ok(Self { args: Some(args) }) + } +} + +impl ToTokens for TypeArgs { + fn to_tokens(&self, tokens: &mut TokenStream) { + if let Some(args) = &self.args { + tokens.extend(quote! { < #args > }); + } + } +} 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..cd954605 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/ident/type_generic_param.rs @@ -0,0 +1,181 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::token::{Colon, Comma, Const, Gt, Lt}; +use syn::{Error, Generics, Ident, Lifetime, Token, Type, parse_quote}; + +/// 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)`. +/// +/// The existing `TypeGenerics` type approximates this by parsing a full +/// `syn::Generics` and then round-tripping it through `split_for_impl` to +/// detect bounds. Modelling the valid forms directly is both clearer and +/// catches more invalid inputs (such as defaults) up front. +#[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(ConstGenericParam), +} + +/// 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(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 optional angle-bracketed parameter list at a type definition site, e.g. +/// the `<'a, C>` in `Bar<'a, C>`. +/// +/// As with [`TypeArgs`], `None` represents no angle brackets while +/// `Some(empty)` represents an explicit empty `<>`. +/// +/// [`TypeArgs`]: crate::types::ident::TypeArgs +#[derive(Debug, Clone, Default)] +pub struct TypeGenericParams { + pub params: Option>, +} + +impl TypeGenericParams { + pub fn make_params(&mut self) -> &mut Punctuated { + self.params.get_or_insert_with(Punctuated::new) + } + + pub fn is_empty(&self) -> bool { + match &self.params { + Some(params) => params.is_empty(), + None => true, + } + } + + /// 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 { + if !input.peek(Lt) { + return Ok(Self { params: None }); + } + + let _: Lt = input.parse()?; + + let mut params = Punctuated::new(); + + while !input.peek(Gt) { + params.push_value(input.parse()?); + + if input.peek(Gt) { + break; + } + + params.push_punct(input.parse()?); + } + + let _: Gt = input.parse()?; + + Ok(Self { + params: Some(params), + }) + } +} + +impl ToTokens for TypeGenericParams { + fn to_tokens(&self, tokens: &mut TokenStream) { + if let Some(params) = &self.params { + tokens.extend(quote! { < #params > }); + } + } +} diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params.rs b/crates/tests/cgp-tests/tests/ident_with_type_params.rs new file mode 100644 index 00000000..b8a9f0d1 --- /dev/null +++ b/crates/tests/cgp-tests/tests/ident_with_type_params.rs @@ -0,0 +1 @@ +pub mod ident_with_type_params_tests; diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs @@ -0,0 +1 @@ + From 72212624362980c10abadb71bf06f65bbda0bd00 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 10:43:28 +0200 Subject: [PATCH 02/15] AI add docs and tests --- Cargo.lock | 4 + .../cgp-macro-core/src/types/ident/README.md | 371 ++++++++++++++++++ .../src/types/ident/draft_tests.rs | 105 ----- .../cgp-macro-core/src/types/ident/mod.rs | 1 - crates/tests/cgp-tests/Cargo.toml | 6 + .../tests/ident_with_type_params_tests/mod.rs | 62 +++ .../new_ident_with_type_args.rs | 142 +++++++ .../new_ident_with_type_generics.rs | 119 ++++++ .../old_constructs.rs | 99 +++++ .../old_vs_new.rs | 83 ++++ .../path_with_type_args.rs | 101 +++++ 11 files changed, 987 insertions(+), 106 deletions(-) delete mode 100644 crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs create mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs create mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs create mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_constructs.rs create mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs create mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs diff --git a/Cargo.lock b/Cargo.lock index 843ce2ff..0b28ed56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,9 +252,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/crates/macros/cgp-macro-core/src/types/ident/README.md b/crates/macros/cgp-macro-core/src/types/ident/README.md index e69de29b..7371c7bb 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/README.md +++ b/crates/macros/cgp-macro-core/src/types/ident/README.md @@ -0,0 +1,371 @@ +# Identifiers and Paths with Type Parameters + +This module provides the parsing constructs that CGP macros use to read +identifiers and paths that carry type parameters, such as `Foo`, +`Bar<'a, C>`, or `path::to::Foo<(A, B), C>`. + +There are two fundamentally different *positions* in which type parameters +appear, and CGP needs to parse both. Getting the distinction right matters, +because the two positions allow different syntax: + +- **Definition site** — where generic parameters are *introduced*, e.g. the + `` in `struct Foo` or the `<'a, C>` in `trait Bar<'a, C>`. Here + each parameter is a *simple, unconstrained binder*: a lifetime, a type + identifier, or a const parameter. +- **Type-expression site** — where generic arguments are *applied*, e.g. the + `` in `Foo` used as a type. Here each argument may be an arbitrary + type, including composite forms such as `(A, B)` or `Bar`. + +The constructs in this module fall into these two groups, plus the supporting +argument/parameter types they are built from. + +| Construct | Position | Head | Status | +|---|---|---|---| +| `IdentWithTypeGenerics` | definition site | `Ident` | original | +| `IdentWithTypeArgs` | type-expression site | `Ident` | original | +| `NewIdentWithTypeGenerics` | definition site | `Ident` | new | +| `NewIdentWithTypeArgs` | type-expression site | `Ident` | new | +| `PathWithTypeArgs` | type-expression site | `syn::Path` | new | + +The behavior described here is exercised by the test suite at +`crates/tests/cgp-tests/tests/ident_with_type_params_tests/`. + +--- + +## Why `syn`'s constructs are not sufficient + +CGP needs *faithful* parsers: parsers that accept exactly the forms that are +valid in a given position, and reject everything else at parse time with a clear +error. The off-the-shelf `syn` constructs do not give us this, for three +different reasons. + +### 1. `syn::AngleBracketedGenericArguments` is too permissive for arguments + +The original `IdentWithTypeArgs` models its argument list with +`syn::AngleBracketedGenericArguments`, whose elements are +[`syn::GenericArgument`]: + +```rust +pub enum GenericArgument { + Lifetime(Lifetime), // 'a — valid here + Type(Type), // A, (A, B) — valid here + Const(Expr), // 3, { N } — valid here + AssocType(AssocType), // Item = T — NOT valid in a type-argument position + AssocConst(AssocConst), // N = 1 — NOT valid in a type-argument position + Constraint(Constraint), // Item: Clone — NOT valid in a type-argument position +} +``` + +The last three variants are only meaningful inside *trait bounds* (e.g. +`dyn Iterator`), not in a plain applied type like a struct field type. +But `AngleBracketedGenericArguments` accepts all six variants unconditionally, so +`IdentWithTypeArgs` silently accepts invalid input such as `Foo` or +`Foo`. + +### 2. `syn::Generics` is the wrong shape for definition-site parameters + +The original `IdentWithTypeGenerics` models its parameter list with +`syn::Generics`. But `Generics` is designed for full `impl`/`struct`/`fn` +headers: every parameter can carry bounds and a default, and the whole thing can +carry a trailing `where` clause. None of that is allowed in the simple +definition-site lists CGP cares about. + +To compensate, the original `TypeGenerics` parser uses a *round-trip hack*: it +parses a full `Generics`, runs it through `split_for_impl()` to obtain the +"type generics" projection (which strips bounds and defaults), re-parses that, +and rejects the input if the two differ: + +```rust +let generics: Generics = input.parse()?; +let (_, type_generics, _) = generics.split_for_impl(); +let generics2: Generics = parse_internal(type_generics.to_token_stream())?; +if generics != generics2 { + return Err(Error::new_spanned(generics, "invalid type generics syntax")); +} +``` + +This works for rejecting bounds and defaults, but it is opaque, it relies on a +structural-equality comparison as a validation mechanism, and it has a surprising +side effect: it rejects **const generics**. The type-generics projection of +`const N: usize` is the bare `N`, which re-parses as a *type* parameter, so the +equality check fails and `Bar` is rejected — even though const +parameters are perfectly valid at a definition site. + +### 3. `syn::Path` buries the final arguments + +CGP frequently parses things that are genuinely Rust *paths* — provider trait +paths, namespace references, preset names — many of which are pulled out of a +real `syn::Path` (for instance from `item_impl.trait_`). The arguments we care +about live on the **final segment**, hidden inside +`path.segments.last().arguments` as a `PathArguments` enum. Reaching them, and +rewriting them (e.g. inserting the `Context` type as the first argument), is +awkward and repetitive. + +Because the original `IdentWithTypeArgs` head is a single `Ident`, it cannot +parse a path at all: `path::to::Foo` is rejected outright. So any site that +wanted to accept a qualified path had no construct to use. + +--- + +## The constructs + +### Supporting types + +#### `TypeArg` / `TypeArgs` (type-expression arguments) + +`TypeArg` is the faithful element of a type-argument list. It is the restriction +of `syn::GenericArgument` to the three valid variants: + +```rust +pub enum TypeArg { + Lifetime(Lifetime), // 'a + Type(Type), // A, (A, B), Bar, &'a A, [A; 4], dyn Tr, ... + Const(Expr), // a literal (3, true) or a braced block ({ N }) +} +``` + +Associated bindings (`Item = T`), associated const bindings (`N = 1`), and +associated bounds (`Item: Clone`) are rejected during parsing. `TypeArg` can +also be produced from an already-parsed `syn::GenericArgument` via +`TypeArg::from_generic_argument`, which applies the same validation (used by +`PathWithTypeArgs`). + +`TypeArgs` is the optional angle-bracketed list: + +```rust +pub struct TypeArgs { + pub args: Option>, +} +``` + +`None` means there were no angle brackets at all (`Foo`); `Some(empty)` means an +explicit empty list (`Foo<>`). `make_args(&mut self)` returns the inner +`Punctuated`, inserting an empty list if necessary — this mirrors the original +`GenericArguments::make_args` so migration is mechanical. + +#### `TypeGenericParam` / `TypeGenericParams` (definition-site parameters) + +`TypeGenericParam` is the faithful element of a definition-site parameter list: + +```rust +pub enum TypeGenericParam { + Lifetime(Lifetime), // 'a + Type(Ident), // C + Const(ConstGenericParam), // const N: usize +} +``` + +Each variant is a *bare* binder: bounds (`A: Clone`, `'a: 'b`) and defaults +(`A = B`, `const N: usize = 0`) are rejected, as are composite forms +(`(A, B)`, `Bar`). `ConstGenericParam` deliberately has no default field. + +`TypeGenericParams` is the optional list, with the same `None` / `Some(empty)` +convention as `TypeArgs` and a `make_params` accessor. It also provides +`to_generics()`, which lowers the parameters into a plain `syn::Generics` for +downstream code that builds struct/impl headers (e.g. `EmptyStruct`). + +### Original constructs + +#### `IdentWithTypeGenerics` = `Ident` + `TypeGenerics` + +A definition-site construct. Accepts `Foo`, `Foo`, `Bar<'a, C>`. Rejects +bounds, defaults, and composite parameters via the `split_for_impl` round-trip +hack — and, as a side effect of that hack, also rejects const parameters +(`Bar`). + +#### `IdentWithTypeArgs` = `Ident` + `GenericArguments` + +A type-expression construct. Accepts `Foo`, `Foo`, composite arguments +(`Foo<(A, B), Bar>`), lifetimes, and const arguments. **Unfaithfully** also +accepts associated bindings and bounds (`Foo`, `Foo`, +`Foo`, `Foo`). The head is a single `Ident`, so paths +(`path::to::Foo`) and turbofish (`Foo::`) are rejected. + +### New constructs + +#### `NewIdentWithTypeGenerics` = `Ident` + `TypeGenericParams` + +The intended replacement for `IdentWithTypeGenerics`. Same accepted forms, but: + +- the parameter list is modelled directly, with no round-trip hack; +- **const parameters are accepted** (`Bar`); +- defaults are rejected up front (the original happens to reject them too, but + only as a side effect of the structural comparison). + +#### `NewIdentWithTypeArgs` = `Ident` + `TypeArgs` + +The intended replacement for `IdentWithTypeArgs`. Same valid forms, but +associated bindings/bounds are rejected at parse time. The head is still a single +`Ident` (paths and turbofish rejected). + +#### `PathWithTypeArgs` = `syn::Path` + `TypeArgs` + +A generalization of `NewIdentWithTypeArgs` from a single-identifier head to a +full path head. It parses a `syn::Path`, then **lifts the final segment's +arguments** out into a separate `type_args: TypeArgs` field, leaving `path` free +of those arguments. This makes the final arguments directly accessible and +rewritable, while `ident()` exposes the final segment's identifier. + +Restrictions enforced during parsing: + +- only the final segment may carry arguments — `path::to::Foo` is rejected; +- turbofish is rejected — `path::to::Foo::` must be written `path::to::Foo`; +- parenthesized arguments are rejected — `Fn(A) -> B`; +- arguments obey the same `TypeArg` rules (no associated bindings/bounds). + +Because any single identifier is also a valid one-segment path, every input that +`NewIdentWithTypeArgs` accepts is also accepted by `PathWithTypeArgs`. +`PathWithTypeArgs` is therefore a strict superset, and is the right default at a +type-expression site unless a qualified path must specifically be forbidden. + +--- + +## Behavior comparison + +### Type-expression position (`IdentWithTypeArgs` vs `NewIdentWithTypeArgs` / `PathWithTypeArgs`) + +| Input | `IdentWithTypeArgs` | `NewIdentWithTypeArgs` | `PathWithTypeArgs` | +|---|---|---|---| +| `Foo` | accept | accept | accept | +| `Foo<>` | accept | accept | accept | +| `Foo` | accept | accept | accept | +| `Foo<(A, B), C>` | accept | accept | accept | +| `Foo, C>` | accept | accept | accept | +| `Foo<'a, A>` | accept | accept | accept | +| `Foo<3>`, `Foo<{ N }>` | accept | accept | accept | +| `Foo` | **accept** ⚠ | reject | reject | +| `Foo` | **accept** ⚠ | reject | reject | +| `Foo` | **accept** ⚠ | reject | reject | +| `Foo` | **accept** ⚠ | reject | reject | +| `path::to::Foo` | reject | reject | **accept** | +| `Foo::` (turbofish) | reject | reject | reject | +| `Fn(A) -> B` | reject | reject | reject | + +⚠ marks the original construct's unfaithful acceptances. + +### Definition-site position (`IdentWithTypeGenerics` vs `NewIdentWithTypeGenerics`) + +| Input | `IdentWithTypeGenerics` | `NewIdentWithTypeGenerics` | +|---|---|---| +| `Foo` | accept | accept | +| `Foo` | accept | accept | +| `Bar<'a, C>` | accept | accept | +| `Bar` | **reject** | **accept** | +| `Foo` | reject | reject | +| `Foo<'a: 'b>` | reject | reject | +| `Foo` | reject | reject | +| `Foo<(A, B)>` | reject | reject | +| `Foo>` | reject | reject | +| `path::to::Foo` | reject | reject | + +--- + +## Forms of generic parameters considered + +For completeness, here is the full matrix of generic-parameter/argument forms in +the Rust grammar and how the new constructs treat each. + +| Form | Example | Def site (`NewIdentWithTypeGenerics`) | Arg site (`NewIdentWithTypeArgs` / `PathWithTypeArgs`) | +|---|---|---|---| +| Lifetime | `'a` | accept | accept | +| Type identifier | `A` | accept | accept (as a `Type`) | +| Composite type | `(A, B)`, `Bar`, `&'a A`, `[A; 4]`, `dyn Tr`, `fn(A) -> B` | reject | accept | +| Const parameter | `const N: usize` | accept | n/a | +| Const argument | `3`, `true`, `{ N }` | n/a | accept | +| Trait/lifetime bound | `A: Clone`, `'a: 'b` | reject | reject | +| Default | `A = B`, `const N: usize = 0` | reject | n/a | +| Associated type binding | `Item = T` | n/a | reject | +| Associated const binding | `N = 1` | n/a | reject | +| Associated type bound | `Item: Clone` | n/a | reject | +| Where clause | `where A: Clone` | not consumed (out of scope) | not consumed | +| Turbofish | `::` | n/a | reject | +| Path head | `path::to::Foo` | reject | `PathWithTypeArgs` only | +| Intermediate-segment args | `path::to::Foo` | n/a | reject | +| Parenthesized args | `Fn(A) -> B` | n/a | reject | + +Two design decisions worth highlighting: + +- **Const generics are supported.** CGP does not appear to use them today, but + they are a legitimate Rust form, so the new constructs accept them rather than + rejecting them by accident (as the original definition-site construct does). +- **A bare-identifier const argument is parsed as a `Type`.** `Foo` is + classified as `TypeArg::Type`, not `TypeArg::Const`, exactly as `syn` does, + because the two are syntactically indistinguishable without resolution. Only + literal or braced const arguments are classified as `Const`. + +--- + +## Migration guide + +The replacements come in two flavors: + +- **Definition sites** → `NewIdentWithTypeGenerics` (a drop-in for + `IdentWithTypeGenerics`). +- **Type-expression sites** → `PathWithTypeArgs` by default, or + `NewIdentWithTypeArgs` where a qualified path must be forbidden. + +The original public surface is mirrored on the new types (`From`, +`ToTokens`, `to_type`/`Into`, `make_args`/`make_params`), so most call +sites change only the type name. The notable structural changes are: argument +iteration now yields `TypeArg` instead of `syn::GenericArgument`, and the path +head is reached through `ident()` / `path` rather than a bare `ident` field. + +### Definition sites → `NewIdentWithTypeGenerics` + +These all introduce a fresh local name, so an ident head is correct. + +| Location | Field | +|---|---| +| `types/cgp_component/args/component_args.rs`, `args/raw.rs` | `component_name` | +| `types/attributes/prefix.rs` | `component_name` parameter | +| `types/namespace/table.rs` | `namespace` (table head, possibly `new`) | +| `types/delegate_component/table/main.rs` | `struct_type` (the `new` table struct) | +| `types/cgp_provider/item.rs` | `provider_type` (the provider struct being defined) | +| `cgp-macro-lib/parse/component_spec.rs` | `component_name` | + +Downstream code that consumes `.type_generics.generics` (a `syn::Generics`) +should call `.type_generics.to_generics()` instead. + +### Type-expression sites → `PathWithTypeArgs` + +These name an existing item (a trait, namespace, preset, or component-name type) +that may legitimately be written as a qualified path. Several already extract +their value from a real `syn::Path`, so moving to `PathWithTypeArgs` both fixes +the faithfulness gap *and* unlocks the `path::to::Foo` use case. + +| Location | Field / use | Notes | +|---|---|---| +| `types/cgp_provider/item.rs` | `provider_trait` | parsed from `item_impl.trait_`; use `.ident()` to build `{Trait}Component` | +| `types/provider_impl.rs` | `provider_path` destructuring | replace `IdentWithTypeArgs { ident, type_args }` with `path`/`type_args` + `ident()` | +| `types/cgp_impl/lowered.rs` | `provider_trait_path` | `make_args().insert(0, ..)` still works (now over `TypeArg`) | +| `types/attributes/use_type/attribute.rs` | `trait_path` | already named a "path" | +| `types/attributes/use_provider/attribute.rs` | `provider_trait_bounds` | trait names may be paths | +| `types/attributes/uses.rs`, `attributes/function.rs`, `attributes/cgp_impl_attributes.rs` | `uses` / `imports` | `#[uses(Trait<..>)]` trait names | +| `types/attributes/prefix.rs`, `attributes/default_impl/attribute.rs` | `namespace` | namespace references | +| `types/namespace/table.rs` | `parent_namespace` | | +| `types/namespace/inherit.rs` | `namespace` | | +| `types/delegate_component/statement/for_loop.rs`, `statement/eval.rs` | `namespace` | | +| `cgp-macro-lib/entrypoints/cgp_inherit.rs` | `preset` | preset names may be paths | +| `cgp-macro-lib/parse/define_preset.rs` | `parent_type`, preset entries | | +| `cgp-macro-lib/entrypoints/cgp_preset.rs`, `preset/impl_is_preset.rs`, `for_each_replace.rs` | `DelegateEntry`/`DelegateKey` | component-name keys | +| `cgp-macro-lib/parse/delegate_components.rs` | `DelegateEntry` | preset form | +| `cgp-macro-lib/parse/check_components.rs` | `context_type` for naming | only `.ident()` is needed | + +### Type-expression sites → `NewIdentWithTypeArgs` + +Use this only where the grammar should specifically forbid a qualified path — +for example a freshly declared local name that nonetheless appears in +argument-list syntax. In practice the "must be a single ident" cases in CGP are +all *definition* sites (covered by `NewIdentWithTypeGenerics`), so +`NewIdentWithTypeArgs` is mostly useful as the precise, path-rejecting option +when reviewing a specific site shows that a path would be meaningless there. + +### `ProviderImplArgs::from_generic_args` + +`types/cgp_provider/provider_impl_args.rs` currently iterates +`syn::GenericArgument` to split the leading `Context` type from the remaining +impl arguments. After migration it should take a `&TypeArgs` (or +`&Punctuated`) and match on `TypeArg::{Lifetime, Type, Const}` — +which is a more direct mapping than the current `GenericArgument` match (the +`_ => Err(..)` arm for unsupported variants disappears, since `TypeArg` has no +invalid variants). diff --git a/crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs b/crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs deleted file mode 100644 index c7b56b11..00000000 --- a/crates/macros/cgp-macro-core/src/types/ident/draft_tests.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![cfg(test)] - -use quote::quote; -use syn::parse2; - -use crate::types::ident::{NewIdentWithTypeArgs, NewIdentWithTypeGenerics, PathWithTypeArgs}; - -fn args_ok(ts: proc_macro2::TokenStream) { - parse2::(ts.clone()) - .unwrap_or_else(|e| panic!("expected ok for `{ts}`: {e}")); -} -fn args_err(ts: proc_macro2::TokenStream) { - assert!( - parse2::(ts.clone()).is_err(), - "expected err for `{ts}`" - ); -} -fn gen_ok(ts: proc_macro2::TokenStream) { - parse2::(ts.clone()) - .unwrap_or_else(|e| panic!("expected ok for `{ts}`: {e}")); -} -fn gen_err(ts: proc_macro2::TokenStream) { - assert!( - parse2::(ts.clone()).is_err(), - "expected err for `{ts}`" - ); -} -fn path_ok(ts: proc_macro2::TokenStream) { - parse2::(ts.clone()) - .unwrap_or_else(|e| panic!("expected ok for `{ts}`: {e}")); -} -fn path_err(ts: proc_macro2::TokenStream) { - assert!( - parse2::(ts.clone()).is_err(), - "expected err for `{ts}`" - ); -} - -// Compare re-emitted tokens by re-parsing both into `syn::Type`, so that -// purely cosmetic spacing differences (e.g. `> >` vs `>>`) are ignored. -fn roundtrip_args(ts: proc_macro2::TokenStream) { - let parsed: NewIdentWithTypeArgs = parse2(ts.clone()).unwrap(); - let a: syn::Type = parse2(quote!(#parsed)).unwrap(); - let b: syn::Type = parse2(ts).unwrap(); - assert_eq!(a, b); -} -fn roundtrip_path(ts: proc_macro2::TokenStream) { - let parsed: PathWithTypeArgs = parse2(ts.clone()).unwrap(); - let a: syn::Type = parse2(quote!(#parsed)).unwrap(); - let b: syn::Type = parse2(ts).unwrap(); - assert_eq!(a, b); -} - -#[test] -fn type_args() { - args_ok(quote!(Foo)); - args_ok(quote!(Foo)); - args_ok(quote!(Foo<(A, B), C>)); - args_ok(quote!(Foo, C>)); - args_ok(quote!(Foo<'a, A>)); - args_ok(quote!(Foo<3>)); - args_ok(quote!(Foo<{ N }>)); - - args_err(quote!(Foo)); - args_err(quote!(Foo)); - args_err(quote!(Foo)); -} - -#[test] -fn type_generics() { - gen_ok(quote!(Foo)); - gen_ok(quote!(Foo)); - gen_ok(quote!(Bar<'a, C>)); - gen_ok(quote!(Bar)); - - gen_err(quote!(Foo)); - gen_err(quote!(Foo)); - gen_err(quote!(Foo<(A, B)>)); - gen_err(quote!(Foo>)); - gen_err(quote!(Bar)); -} - -#[test] -fn paths() { - path_ok(quote!(Foo)); - path_ok(quote!(Foo)); - path_ok(quote!(path::to::Foo)); - path_ok(quote!(path::to::Foo)); - path_ok(quote!(path::to::Bar<(A, B), B>)); - - path_err(quote!(path::to::Foo)); - path_err(quote!(path::to::Foo)); - path_err(quote!(path::to::Foo::)); - - let parsed: PathWithTypeArgs = parse2(quote!(path::to::Foo)).unwrap(); - assert_eq!(parsed.ident().to_string(), "Foo"); -} - -#[test] -fn roundtrips() { - roundtrip_args(quote!(Foo)); - roundtrip_args(quote!(Foo<(A, B), Bar>)); - roundtrip_path(quote!(path::to::Foo)); - roundtrip_path(quote!(::path::to::Foo<'a, A>)); -} 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 898efe80..97083445 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/mod.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/mod.rs @@ -1,4 +1,3 @@ -mod draft_tests; mod ident_with_type_args; mod ident_with_type_generics; mod new_ident_with_type_args; 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" } diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs index 8b137891..17e76be9 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs @@ -1 +1,63 @@ +//! Tests exploring the parsing behavior of the ident/path-with-type-parameter +//! constructs in `cgp-macro-core`. +//! +//! These cover both the original constructs (`IdentWithTypeArgs`, +//! `IdentWithTypeGenerics`) and the new ones (`NewIdentWithTypeArgs`, +//! `NewIdentWithTypeGenerics`, `PathWithTypeArgs`), including a side-by-side +//! comparison that documents exactly where their behaviors diverge. +//! +//! See `crates/macros/cgp-macro-core/src/types/ident/README.md` for the prose +//! explanation that accompanies these tests. +pub mod new_ident_with_type_args; +pub mod new_ident_with_type_generics; +pub mod old_constructs; +pub mod old_vs_new; +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-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs new file mode 100644 index 00000000..55f8b649 --- /dev/null +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs @@ -0,0 +1,142 @@ +//! Corner cases for `NewIdentWithTypeArgs` — an identifier followed by an +//! optional *type-expression* argument list, e.g. `Foo`. + +use cgp_macro_core::types::ident::{NewIdentWithTypeArgs, TypeArg}; +use quote::quote; +use syn::parse2; + +use super::{assert_idempotent, assert_parses, assert_rejects}; + +type Subject = NewIdentWithTypeArgs; + +#[test] +fn accepts_bare_ident() { + assert_parses::(quote!(Foo)); +} + +#[test] +fn accepts_empty_argument_list() { + // An explicit empty `<>` is allowed and distinct 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.expect("expected an argument list"); + + 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_none()); + assert!(parsed.type_args.is_empty()); +} + +#[test] +fn empty_brackets_are_distinct_from_no_brackets() { + let bare: Subject = parse2(quote!(Foo)).unwrap(); + let empty: Subject = parse2(quote!(Foo)).unwrap(); + + assert!(bare.type_args.args.is_none()); + assert!(empty.type_args.args.is_some()); + assert!(empty.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-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs new file mode 100644 index 00000000..bb60a3e7 --- /dev/null +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs @@ -0,0 +1,119 @@ +//! Corner cases for `NewIdentWithTypeGenerics` — an identifier followed by an +//! optional *definition-site* generic parameter list, e.g. `Foo` or +//! `Bar<'a, C>`. + +use cgp_macro_core::types::ident::{NewIdentWithTypeGenerics, TypeGenericParam}; +use quote::quote; +use syn::parse2; + +use super::{assert_idempotent, assert_parses, assert_rejects}; + +type Subject = NewIdentWithTypeGenerics; + +#[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 + .expect("expected a parameter list"); + + 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-tests/tests/ident_with_type_params_tests/old_constructs.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_constructs.rs new file mode 100644 index 00000000..0e89d626 --- /dev/null +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_constructs.rs @@ -0,0 +1,99 @@ +//! Behavior of the original constructs, `IdentWithTypeArgs` and +//! `IdentWithTypeGenerics`. +//! +//! These tests document the existing behavior as-is, including the cases where +//! the original constructs are *too permissive* (accepting forms that are not +//! valid in the relevant position). The new constructs tighten exactly these +//! cases; see `old_vs_new.rs` for the side-by-side comparison. + +use cgp_macro_core::types::ident::{IdentWithTypeArgs, IdentWithTypeGenerics}; +use quote::quote; + +use super::{assert_parses, assert_rejects}; + +mod ident_with_type_args { + use super::*; + + type Subject = IdentWithTypeArgs; + + #[test] + fn accepts_valid_forms() { + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo<(A, B), C>)); + assert_parses::(quote!(Foo>)); + assert_parses::(quote!(Foo<'a, A>)); + assert_parses::(quote!(Foo<3>)); + } + + // The following four cases demonstrate the unfaithful behavior: these are + // accepted because `IdentWithTypeArgs` delegates to + // `syn::AngleBracketedGenericArguments`, which permits associated bindings + // and bounds that are invalid in a plain type-argument position. + + #[test] + fn unfaithfully_accepts_associated_type_binding() { + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); + } + + #[test] + fn unfaithfully_accepts_associated_const_binding() { + assert_parses::(quote!(Foo)); + } + + #[test] + fn unfaithfully_accepts_associated_type_bound() { + assert_parses::(quote!(Foo)); + } + + #[test] + fn rejects_path_head() { + // The head is always a single `Ident`, so paths cannot be parsed even + // where the source genuinely is a `syn::Path`. + assert_rejects::(quote!(path::to::Foo)); + } + + #[test] + fn rejects_turbofish() { + assert_rejects::(quote!(Foo::)); + } +} + +mod ident_with_type_generics { + use super::*; + + type Subject = IdentWithTypeGenerics; + + #[test] + fn accepts_valid_forms() { + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Foo)); + assert_parses::(quote!(Bar<'a, C>)); + } + + #[test] + fn rejects_bounds_via_roundtrip_hack() { + assert_rejects::(quote!(Foo)); + } + + #[test] + fn rejects_defaults_via_roundtrip_hack() { + assert_rejects::(quote!(Foo)); + } + + #[test] + fn rejects_composite_parameters() { + assert_rejects::(quote!(Foo<(A, B)>)); + assert_rejects::(quote!(Foo>)); + } + + #[test] + fn rejects_const_parameters() { + // The `split_for_impl` round-trip collapses a const parameter into a + // bare type parameter, so the equality check fails and the input is + // rejected. (The new `NewIdentWithTypeGenerics` accepts this form.) + assert_rejects::(quote!(Bar)); + } +} diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs new file mode 100644 index 00000000..b0c8375c --- /dev/null +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs @@ -0,0 +1,83 @@ +//! Side-by-side comparison of the old and new constructs, documenting exactly +//! where their parsing behavior diverges. + +use cgp_macro_core::types::ident::{ + IdentWithTypeArgs, IdentWithTypeGenerics, NewIdentWithTypeArgs, NewIdentWithTypeGenerics, +}; +use quote::quote; + +use super::{assert_parses, assert_rejects}; + +/// Type-argument position: forms that the *old* `IdentWithTypeArgs` accepts but +/// the *new* `NewIdentWithTypeArgs` correctly rejects. +#[test] +fn args_new_is_stricter_about_associated_bindings_and_bounds() { + for tokens in [ + quote!(Foo), + quote!(Foo), + quote!(Foo), + quote!(Foo), + ] { + assert_parses::(tokens.clone()); + assert_rejects::(tokens); + } +} + +/// Type-argument position: forms that both constructs accept identically. +#[test] +fn args_agree_on_valid_forms() { + for tokens in [ + quote!(Foo), + quote!(Foo), + quote!(Foo<(A, B), C>), + quote!(Foo, C>), + quote!(Foo<'a, A>), + quote!(Foo<3>), + ] { + assert_parses::(tokens.clone()); + assert_parses::(tokens); + } +} + +/// Type-argument position: forms that both constructs reject identically. +#[test] +fn args_agree_on_rejected_forms() { + for tokens in [quote!(path::to::Foo), quote!(Foo::)] { + assert_rejects::(tokens.clone()); + assert_rejects::(tokens); + } +} + +/// Definition-site position: forms that both constructs reject identically. +#[test] +fn generics_agree_on_rejected_forms() { + for tokens in [ + quote!(Foo), + quote!(Foo), + quote!(Foo<(A, B)>), + quote!(Foo>), + ] { + assert_rejects::(tokens.clone()); + assert_rejects::(tokens); + } +} + +/// Definition-site position: const generics are the one form where the *new* +/// construct is more permissive than the old one — the old construct rejects +/// them as a side effect of its `split_for_impl` round-trip check. +#[test] +fn generics_new_accepts_const_parameters() { + let tokens = quote!(Bar); + + assert_rejects::(tokens.clone()); + assert_parses::(tokens); +} + +/// Definition-site position: forms that both constructs accept identically. +#[test] +fn generics_agree_on_valid_forms() { + for tokens in [quote!(Foo), quote!(Foo), quote!(Bar<'a, C>)] { + assert_parses::(tokens.clone()); + assert_parses::(tokens); + } +} diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs new file mode 100644 index 00000000..c9e9db85 --- /dev/null +++ b/crates/tests/cgp-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.expect("expected an argument list"); + 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_none()); + 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>)); +} From 225c46a4e0f4b71327e82fbf92a98109e05f561e Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:09:35 +0200 Subject: [PATCH 03/15] AI-migrate macro-core --- .../types/attributes/cgp_impl_attributes.rs | 4 +-- .../attributes/default_impl/attribute.rs | 4 +-- .../src/types/attributes/function.rs | 9 +++--- .../src/types/attributes/prefix.rs | 8 ++--- .../attributes/use_provider/attribute.rs | 4 +-- .../types/attributes/use_type/attribute.rs | 11 ++++--- .../src/types/attributes/uses.rs | 4 +-- .../cgp_component/args/component_args.rs | 6 ++-- .../src/types/cgp_component/args/raw.rs | 4 +-- .../types/cgp_component/preprocessed/item.rs | 2 +- .../src/types/cgp_impl/lowered.rs | 4 +-- .../src/types/cgp_provider/item.rs | 10 +++---- .../types/cgp_provider/provider_impl_args.rs | 18 +++++------ .../delegate_component/statement/eval.rs | 30 +++++++++++-------- .../delegate_component/statement/for_loop.rs | 4 +-- .../types/delegate_component/table/main.rs | 6 ++-- .../src/types/namespace/inherit.rs | 4 +-- .../src/types/namespace/table.rs | 12 ++++---- .../cgp-macro-core/src/types/provider_impl.rs | 11 ++++--- 19 files changed, 81 insertions(+), 74 deletions(-) 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..691c6db6 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 { 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..2455e9bd 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs @@ -4,20 +4,20 @@ use syn::token::In; use crate::exports::RedirectLookup; use crate::functions::parse_internal; -use crate::types::ident::{IdentWithTypeArgs, IdentWithTypeGenerics}; +use crate::types::ident::{NewIdentWithTypeGenerics, 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 { pub fn to_namespace_impl( &self, - component_name: &IdentWithTypeGenerics, + component_name: &NewIdentWithTypeGenerics, ) -> syn::Result { let mut namespace = self.namespace.clone(); namespace @@ -30,7 +30,7 @@ impl PrefixAttribute { let mut type_generics = component_name.type_generics.clone(); type_generics - .params + .make_params() .insert(0, parse_internal!(__Components__)); let item_impl = parse_internal! { 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..e88836b1 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 { 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..3f7ed813 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::{NewIdentWithTypeArgs, PathWithTypeArgs}; #[derive(Clone)] pub struct UseTypeAttribute { pub context_type: Type, - pub trait_path: IdentWithTypeArgs, + pub trait_path: PathWithTypeArgs, pub type_idents: Vec, } @@ -34,7 +34,10 @@ impl Parse for UseTypeAttribute { let (context_type, body) = if input.peek(At) { let _: At = input.parse()?; - let context_type: Type = input.parse::()?.into(); + // The context type is followed by a `::`-separated trait path, so it + // must parse only a single identifier head; a full path parser would + // greedily consume the trailing `::Trait::Type`. + let context_type: Type = input.parse::()?.into(); let _: Colon = input.parse()?; let _: Colon = input.parse()?; @@ -51,7 +54,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/args/component_args.rs b/crates/macros/cgp-macro-core/src/types/cgp_component/args/component_args.rs index ad9f763b..17a355af 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_component/args/component_args.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_component/args/component_args.rs @@ -4,13 +4,13 @@ use syn::{Error, Ident}; use crate::types::attributes::DeriveDelegateAttributes; use crate::types::cgp_component::CgpComponentRawArgs; -use crate::types::ident::IdentWithTypeGenerics; +use crate::types::ident::NewIdentWithTypeGenerics; #[derive(Clone)] pub struct CgpComponentArgs { pub context_ident: Ident, pub provider_ident: Ident, - pub component_name: IdentWithTypeGenerics, + pub component_name: NewIdentWithTypeGenerics, pub derive_delegate_attributes: DeriveDelegateAttributes, } @@ -35,7 +35,7 @@ impl TryFrom for CgpComponentArgs { .unwrap_or_else(|| Ident::new("__Context__", Span::call_site())); let component_name = raw_args.component_name.unwrap_or_else(|| { - IdentWithTypeGenerics::from(Ident::new( + NewIdentWithTypeGenerics::from(Ident::new( &format!("{provider_ident}Component"), Span::call_site(), )) diff --git a/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs b/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs index 1a87f01b..2d961618 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs @@ -3,13 +3,13 @@ use syn::token::{Colon, Comma}; use syn::{Error, Ident}; use crate::types::attributes::DeriveDelegateAttributes; -use crate::types::ident::IdentWithTypeGenerics; +use crate::types::ident::NewIdentWithTypeGenerics; #[derive(Default)] pub struct CgpComponentRawArgs { pub context_ident: Option, pub provider_ident: Option, - pub component_name: Option, + pub component_name: Option, pub derive_delegate_attributes: Option, } 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..7d3a4f18 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, } 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..e32877dc 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::{NewIdentWithTypeGenerics, 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(), ); @@ -57,11 +57,11 @@ impl ItemCgpProvider { let impl_self_type = &provider_impl.self_ty; - let provider_type: IdentWithTypeGenerics = parse_internal!( #impl_self_type ); + let provider_type: NewIdentWithTypeGenerics = parse_internal!( #impl_self_type ); 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..fceb551f 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,27 +18,27 @@ 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 { + for arg in args { match arg { - GenericArgument::Lifetime(life) => { + TypeArg::Lifetime(life) => { impl_args.push(ProviderImplArg::Life(life.clone())); } - GenericArgument::Type(ty) => { + 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( - arg.span(), - format!("unsupported type argument: {:?}", arg), + 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..d0a72d5a 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, TypeArg}; 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,21 @@ 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: Vec<&TypeArg> = match &self.namespace.type_args.args { + Some(args) => args.iter().collect(), + None => Vec::new(), + }; + + 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..f986ea9f 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 @@ -8,7 +8,7 @@ use crate::traits::ParseOptionalKeyword; use crate::types::delegate_component::{DelegateEntries, ExtractInnerDelegateTables}; use crate::types::empty_struct::EmptyStruct; use crate::types::generics::ImplGenerics; -use crate::types::ident::IdentWithTypeGenerics; +use crate::types::ident::NewIdentWithTypeGenerics; use crate::types::keyword::Keyword; use crate::types::keywords::New; @@ -54,11 +54,11 @@ impl DelegateTable { let mut item_structs = Vec::new(); if self.new.is_some() { - let struct_type: IdentWithTypeGenerics = + let struct_type: NewIdentWithTypeGenerics = 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/namespace/inherit.rs b/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs index 836fe8ec..2e46852c 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, } 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..c88bb46b 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::{NewIdentWithTypeGenerics, PathWithTypeArgs}; use crate::types::keyword::Keyword; use crate::types::keywords::New; use crate::types::namespace::{EvaluatedNamespaceTable, InheritNamespaceStatement}; @@ -16,8 +16,8 @@ use crate::types::namespace::{EvaluatedNamespaceTable, InheritNamespaceStatement pub struct NamespaceTable { pub impl_generics: ImplGenerics, pub new: Option>, - pub namespace: IdentWithTypeGenerics, - pub parent_namespace: Option<(Colon, IdentWithTypeArgs)>, + pub namespace: NewIdentWithTypeGenerics, + pub parent_namespace: Option<(Colon, PathWithTypeArgs)>, pub entries: DelegateEntries, } @@ -57,7 +57,9 @@ impl NamespaceTable { pub fn build_namespace_trait(&self) -> syn::Result { let namespace_ident = &self.namespace.ident; let mut namespace_generics = self.namespace.type_generics.clone(); - namespace_generics.params.push(parse_internal!(__Table__)); + namespace_generics + .make_params() + .push(parse_internal!(__Table__)); let namespace_trait: Type = parse_internal!( #namespace_ident #namespace_generics ); Ok(namespace_trait) @@ -109,7 +111,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 = From 4c04171729d50578dd6acdf84b79aa3aca9c1256 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:19:28 +0200 Subject: [PATCH 04/15] AI-migrate cgp-macro-lib --- crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs | 6 +++--- crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs | 4 ++-- crates/macros/cgp-macro-lib/src/for_each_replace.rs | 6 +++--- crates/macros/cgp-macro-lib/src/parse/check_components.rs | 4 ++-- crates/macros/cgp-macro-lib/src/parse/component_spec.rs | 6 +++--- crates/macros/cgp-macro-lib/src/parse/define_preset.rs | 6 +++--- .../src/parse/delegate_and_check_components.rs | 4 ++-- .../macros/cgp-macro-lib/src/parse/delegate_components.rs | 6 +++--- crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs | 6 +++--- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs index 4d375d6f..8bfa43b4 100644 --- a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs +++ b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs @@ -1,5 +1,5 @@ use cgp_macro_core::types::generics::TypeGenerics; -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use proc_macro2::TokenStream; use quote::quote; use syn::{Ident, ItemImpl, ItemStruct, parse_quote, parse2}; @@ -7,7 +7,7 @@ use syn::{Ident, ItemImpl, ItemStruct, parse_quote, parse2}; pub fn cgp_inherit(attr: TokenStream, body: TokenStream) -> syn::Result { let context_struct: ItemStruct = parse2(body)?; - let preset: IdentWithTypeArgs = parse2(attr)?; + let preset: NewIdentWithTypeArgs = parse2(attr)?; let type_generics = TypeGenerics::try_from(&context_struct.generics)?; @@ -26,7 +26,7 @@ pub fn cgp_inherit(attr: TokenStream, body: TokenStream) -> syn::Result, - preset: &IdentWithTypeArgs, + preset: &NewIdentWithTypeArgs, ) -> syn::Result<(ItemImpl, ItemImpl)> { let preset_name = &preset.ident; let preset_generics = &preset.type_args; diff --git a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs index 089f78e5..39dc3261 100644 --- a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs +++ b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use cgp_macro_core::functions::to_snake_case_str; use cgp_macro_core::types::empty_struct::EmptyStruct; use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, TokenStreamExt, quote}; use syn::punctuated::Punctuated; @@ -17,7 +17,7 @@ use crate::preset::{define_substitution_macro, impl_components_is_preset}; pub fn define_preset(body: TokenStream) -> syn::Result { let ast: DefinePreset = syn::parse2(body)?; - let delegate_entries: Punctuated, Comma> = ast + let delegate_entries: Punctuated, Comma> = ast .delegate_entries .iter() .map(|entry| entry.entry.clone()) diff --git a/crates/macros/cgp-macro-lib/src/for_each_replace.rs b/crates/macros/cgp-macro-lib/src/for_each_replace.rs index 6133e13f..957a46ef 100644 --- a/crates/macros/cgp-macro-lib/src/for_each_replace.rs +++ b/crates/macros/cgp-macro-lib/src/for_each_replace.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use proc_macro2::{Group, TokenStream, TokenTree}; use quote::ToTokens; use syn::__private::parse_brackets; @@ -20,10 +20,10 @@ pub struct ReplaceSpecs { impl Parse for ReplaceSpecs { fn parse(input: ParseStream) -> syn::Result { - let raw_replacements: Vec> = { + let raw_replacements: Vec> = { let content = parse_brackets(input)?.content; let types = - , Comma>>::parse_terminated(&content)?; + , Comma>>::parse_terminated(&content)?; types.into_iter().collect() }; diff --git a/crates/macros/cgp-macro-lib/src/parse/check_components.rs b/crates/macros/cgp-macro-lib/src/parse/check_components.rs index 924d091f..1be8b8a1 100644 --- a/crates/macros/cgp-macro-lib/src/parse/check_components.rs +++ b/crates/macros/cgp-macro-lib/src/parse/check_components.rs @@ -1,5 +1,5 @@ use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use proc_macro2::Span; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; @@ -96,7 +96,7 @@ impl Parse for CheckComponents { let trait_name = if let Some(check_trait_name) = m_check_trait_name { check_trait_name } else { - let context_type: IdentWithTypeArgs = parse2(context_type.to_token_stream())?; + let context_type: NewIdentWithTypeArgs = parse2(context_type.to_token_stream())?; Ident::new( &format!("__Check{}", context_type.ident), diff --git a/crates/macros/cgp-macro-lib/src/parse/component_spec.rs b/crates/macros/cgp-macro-lib/src/parse/component_spec.rs index 05c840c3..73acc36b 100644 --- a/crates/macros/cgp-macro-lib/src/parse/component_spec.rs +++ b/crates/macros/cgp-macro-lib/src/parse/component_spec.rs @@ -2,7 +2,7 @@ use alloc::format; use std::collections::BTreeMap; use cgp_macro_core::types::cgp_component::DeriveDelegateAttributes; -use cgp_macro_core::types::ident::IdentWithTypeGenerics; +use cgp_macro_core::types::ident::NewIdentWithTypeGenerics; use proc_macro2::{Span, TokenStream}; use syn::parse::{End, Parse, ParseStream}; use syn::{Error, Ident, parse2}; @@ -12,7 +12,7 @@ use crate::parse::Entries; pub struct CgpComponentArgs { pub provider_ident: Ident, pub context_ident: Ident, - pub component_name: IdentWithTypeGenerics, + pub component_name: NewIdentWithTypeGenerics, pub derive_delegate_attributes: DeriveDelegateAttributes, } @@ -84,7 +84,7 @@ impl CgpComponentArgs { if let Some(raw_component_name) = raw_component_name { parse2(raw_component_name.clone())? } else { - IdentWithTypeGenerics::from(Ident::new( + NewIdentWithTypeGenerics::from(Ident::new( &format!("{provider_name}Component"), provider_name.span(), )) diff --git a/crates/macros/cgp-macro-lib/src/parse/define_preset.rs b/crates/macros/cgp-macro-lib/src/parse/define_preset.rs index 3de9e30d..d06a5479 100644 --- a/crates/macros/cgp-macro-lib/src/parse/define_preset.rs +++ b/crates/macros/cgp-macro-lib/src/parse/define_preset.rs @@ -1,4 +1,4 @@ -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use proc_macro2::TokenStream; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; @@ -17,7 +17,7 @@ pub struct DefinePreset { pub struct DelegatePresetEntry { pub is_override: Option, - pub entry: DelegateEntry, + pub entry: DelegateEntry, } impl Parse for DefinePreset { @@ -82,7 +82,7 @@ impl Parse for DelegatePresetEntry { #[derive(Clone)] pub struct PresetParent { pub has_expanded: Option, - pub parent_type: IdentWithTypeArgs, + pub parent_type: NewIdentWithTypeArgs, } impl Parse for PresetParent { diff --git a/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs b/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs index 0c11087d..b71b7991 100644 --- a/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs +++ b/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs @@ -1,7 +1,7 @@ use core::iter; use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; @@ -46,7 +46,7 @@ impl Parse for DelegateAndCheckSpec { let trait_name = match m_trait_name { Some(ident) => ident, None => { - let context_type: IdentWithTypeArgs = parse2(context_type.to_token_stream())?; + let context_type: NewIdentWithTypeArgs = parse2(context_type.to_token_stream())?; Ident::new( &format!("__CanUse{}", context_type.ident), context_type.span(), diff --git a/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs b/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs index 736aaf1d..5ff41b62 100644 --- a/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs +++ b/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs @@ -2,7 +2,7 @@ use core::iter; use cgp_macro_core::functions::merge_generics; use cgp_macro_core::types::generics::{ImplGenerics, TypeGenerics}; -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; use syn::parse::discouraged::Speculative; @@ -129,14 +129,14 @@ impl Parse for DelegateEntry { } } -impl Parse for DelegateEntry { +impl Parse for DelegateEntry { fn parse(input: ParseStream) -> syn::Result { let components = if input.peek(Bracket) { let components_body; bracketed!(components_body in input); components_body.parse_terminated(DelegateKey::parse, Token![,])? } else { - let component: DelegateKey = input.parse()?; + let component: DelegateKey = input.parse()?; Punctuated::from_iter(iter::once(component)) }; diff --git a/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs b/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs index 72336da1..6cac27eb 100644 --- a/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs +++ b/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::IdentWithTypeArgs; +use cgp_macro_core::types::ident::NewIdentWithTypeArgs; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{Ident, ItemImpl, Type, parse_quote}; @@ -12,7 +12,7 @@ pub fn impl_components_is_preset( trait_name: &Ident, preset_type: &Type, preset_generics: &ImplGenerics, - delegate_entries: &Punctuated, Comma>, + delegate_entries: &Punctuated, Comma>, ) -> Vec { delegate_entries .iter() @@ -28,7 +28,7 @@ pub fn impl_component_is_preset( trait_name: &Ident, _preset_type: &Type, _preset_generics: &ImplGenerics, - component: &DelegateKey, + component: &DelegateKey, ) -> ItemImpl { let component_type = &component.ty; From 593aa6709336a861f8bf1bbfc4f50c2e2a8475bf Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:20:16 +0200 Subject: [PATCH 05/15] Remove old constructs --- .../cgp-macro-core/src/types/ident/mod.rs | 4 - .../tests/ident_with_type_params_tests/mod.rs | 2 - .../old_constructs.rs | 99 ------------------- .../old_vs_new.rs | 83 ---------------- 4 files changed, 188 deletions(-) delete mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_constructs.rs delete mode 100644 crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs 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 97083445..2ab4fcee 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/mod.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/mod.rs @@ -1,13 +1,9 @@ -mod ident_with_type_args; -mod ident_with_type_generics; mod new_ident_with_type_args; mod new_ident_with_type_generics; mod path_with_type_args; mod type_arg; mod type_generic_param; -pub use ident_with_type_args::*; -pub use ident_with_type_generics::*; pub use new_ident_with_type_args::*; pub use new_ident_with_type_generics::*; pub use path_with_type_args::*; diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs index 17e76be9..9b23e080 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs @@ -11,8 +11,6 @@ pub mod new_ident_with_type_args; pub mod new_ident_with_type_generics; -pub mod old_constructs; -pub mod old_vs_new; pub mod path_with_type_args; use proc_macro2::TokenStream; diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_constructs.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_constructs.rs deleted file mode 100644 index 0e89d626..00000000 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_constructs.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Behavior of the original constructs, `IdentWithTypeArgs` and -//! `IdentWithTypeGenerics`. -//! -//! These tests document the existing behavior as-is, including the cases where -//! the original constructs are *too permissive* (accepting forms that are not -//! valid in the relevant position). The new constructs tighten exactly these -//! cases; see `old_vs_new.rs` for the side-by-side comparison. - -use cgp_macro_core::types::ident::{IdentWithTypeArgs, IdentWithTypeGenerics}; -use quote::quote; - -use super::{assert_parses, assert_rejects}; - -mod ident_with_type_args { - use super::*; - - type Subject = IdentWithTypeArgs; - - #[test] - fn accepts_valid_forms() { - assert_parses::(quote!(Foo)); - assert_parses::(quote!(Foo)); - assert_parses::(quote!(Foo)); - assert_parses::(quote!(Foo<(A, B), C>)); - assert_parses::(quote!(Foo>)); - assert_parses::(quote!(Foo<'a, A>)); - assert_parses::(quote!(Foo<3>)); - } - - // The following four cases demonstrate the unfaithful behavior: these are - // accepted because `IdentWithTypeArgs` delegates to - // `syn::AngleBracketedGenericArguments`, which permits associated bindings - // and bounds that are invalid in a plain type-argument position. - - #[test] - fn unfaithfully_accepts_associated_type_binding() { - assert_parses::(quote!(Foo)); - assert_parses::(quote!(Foo)); - } - - #[test] - fn unfaithfully_accepts_associated_const_binding() { - assert_parses::(quote!(Foo)); - } - - #[test] - fn unfaithfully_accepts_associated_type_bound() { - assert_parses::(quote!(Foo)); - } - - #[test] - fn rejects_path_head() { - // The head is always a single `Ident`, so paths cannot be parsed even - // where the source genuinely is a `syn::Path`. - assert_rejects::(quote!(path::to::Foo)); - } - - #[test] - fn rejects_turbofish() { - assert_rejects::(quote!(Foo::)); - } -} - -mod ident_with_type_generics { - use super::*; - - type Subject = IdentWithTypeGenerics; - - #[test] - fn accepts_valid_forms() { - assert_parses::(quote!(Foo)); - assert_parses::(quote!(Foo)); - assert_parses::(quote!(Bar<'a, C>)); - } - - #[test] - fn rejects_bounds_via_roundtrip_hack() { - assert_rejects::(quote!(Foo)); - } - - #[test] - fn rejects_defaults_via_roundtrip_hack() { - assert_rejects::(quote!(Foo)); - } - - #[test] - fn rejects_composite_parameters() { - assert_rejects::(quote!(Foo<(A, B)>)); - assert_rejects::(quote!(Foo>)); - } - - #[test] - fn rejects_const_parameters() { - // The `split_for_impl` round-trip collapses a const parameter into a - // bare type parameter, so the equality check fails and the input is - // rejected. (The new `NewIdentWithTypeGenerics` accepts this form.) - assert_rejects::(quote!(Bar)); - } -} diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs deleted file mode 100644 index b0c8375c..00000000 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/old_vs_new.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Side-by-side comparison of the old and new constructs, documenting exactly -//! where their parsing behavior diverges. - -use cgp_macro_core::types::ident::{ - IdentWithTypeArgs, IdentWithTypeGenerics, NewIdentWithTypeArgs, NewIdentWithTypeGenerics, -}; -use quote::quote; - -use super::{assert_parses, assert_rejects}; - -/// Type-argument position: forms that the *old* `IdentWithTypeArgs` accepts but -/// the *new* `NewIdentWithTypeArgs` correctly rejects. -#[test] -fn args_new_is_stricter_about_associated_bindings_and_bounds() { - for tokens in [ - quote!(Foo), - quote!(Foo), - quote!(Foo), - quote!(Foo), - ] { - assert_parses::(tokens.clone()); - assert_rejects::(tokens); - } -} - -/// Type-argument position: forms that both constructs accept identically. -#[test] -fn args_agree_on_valid_forms() { - for tokens in [ - quote!(Foo), - quote!(Foo), - quote!(Foo<(A, B), C>), - quote!(Foo, C>), - quote!(Foo<'a, A>), - quote!(Foo<3>), - ] { - assert_parses::(tokens.clone()); - assert_parses::(tokens); - } -} - -/// Type-argument position: forms that both constructs reject identically. -#[test] -fn args_agree_on_rejected_forms() { - for tokens in [quote!(path::to::Foo), quote!(Foo::)] { - assert_rejects::(tokens.clone()); - assert_rejects::(tokens); - } -} - -/// Definition-site position: forms that both constructs reject identically. -#[test] -fn generics_agree_on_rejected_forms() { - for tokens in [ - quote!(Foo), - quote!(Foo), - quote!(Foo<(A, B)>), - quote!(Foo>), - ] { - assert_rejects::(tokens.clone()); - assert_rejects::(tokens); - } -} - -/// Definition-site position: const generics are the one form where the *new* -/// construct is more permissive than the old one — the old construct rejects -/// them as a side effect of its `split_for_impl` round-trip check. -#[test] -fn generics_new_accepts_const_parameters() { - let tokens = quote!(Bar); - - assert_rejects::(tokens.clone()); - assert_parses::(tokens); -} - -/// Definition-site position: forms that both constructs accept identically. -#[test] -fn generics_agree_on_valid_forms() { - for tokens in [quote!(Foo), quote!(Foo), quote!(Bar<'a, C>)] { - assert_parses::(tokens.clone()); - assert_parses::(tokens); - } -} From a547a0b6f2546f117e0d31cb4a3a9a1cd0c55f40 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:22:32 +0200 Subject: [PATCH 06/15] Remove New prefix --- .../src/types/attributes/prefix.rs | 4 +- .../types/attributes/use_type/attribute.rs | 4 +- .../cgp_component/args/component_args.rs | 6 +- .../src/types/cgp_component/args/raw.rs | 4 +- .../src/types/cgp_provider/item.rs | 4 +- .../types/delegate_component/table/main.rs | 4 +- .../src/types/ident/ident_with_type_args.rs | 25 +++++++- .../types/ident/ident_with_type_generics.rs | 15 ++++- .../cgp-macro-core/src/types/ident/mod.rs | 8 +-- .../types/ident/new_ident_with_type_args.rs | 62 ------------------- .../ident/new_ident_with_type_generics.rs | 55 ---------------- .../src/types/ident/path_with_type_args.rs | 6 +- .../src/types/namespace/table.rs | 4 +- .../src/entrypoints/cgp_inherit.rs | 6 +- .../src/entrypoints/cgp_preset.rs | 4 +- .../cgp-macro-lib/src/for_each_replace.rs | 6 +- .../src/parse/check_components.rs | 4 +- .../cgp-macro-lib/src/parse/define_preset.rs | 6 +- .../parse/delegate_and_check_components.rs | 4 +- .../src/parse/delegate_components.rs | 6 +- .../src/preset/impl_is_preset.rs | 6 +- .../tests/ident_with_type_params_tests/mod.rs | 4 +- .../new_ident_with_type_args.rs | 6 +- .../new_ident_with_type_generics.rs | 6 +- 24 files changed, 85 insertions(+), 174 deletions(-) delete mode 100644 crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_args.rs delete mode 100644 crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs 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 2455e9bd..6dc0a007 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs @@ -4,7 +4,7 @@ use syn::token::In; use crate::exports::RedirectLookup; use crate::functions::parse_internal; -use crate::types::ident::{NewIdentWithTypeGenerics, PathWithTypeArgs}; +use crate::types::ident::{IdentWithTypeGenerics, PathWithTypeArgs}; use crate::types::path::UniPath; #[derive(Clone)] @@ -17,7 +17,7 @@ pub struct PrefixAttribute { impl PrefixAttribute { pub fn to_namespace_impl( &self, - component_name: &NewIdentWithTypeGenerics, + component_name: &IdentWithTypeGenerics, ) -> syn::Result { let mut namespace = self.namespace.clone(); namespace 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 3f7ed813..424fcc75 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,7 +4,7 @@ use syn::{Ident, Type, braced}; use crate::parse_internal; use crate::types::attributes::UseTypeIdent; -use crate::types::ident::{NewIdentWithTypeArgs, PathWithTypeArgs}; +use crate::types::ident::{IdentWithTypeArgs, PathWithTypeArgs}; #[derive(Clone)] pub struct UseTypeAttribute { @@ -37,7 +37,7 @@ impl Parse for UseTypeAttribute { // The context type is followed by a `::`-separated trait path, so it // must parse only a single identifier head; a full path parser would // greedily consume the trailing `::Trait::Type`. - let context_type: Type = input.parse::()?.into(); + let context_type: Type = input.parse::()?.into(); let _: Colon = input.parse()?; let _: Colon = input.parse()?; diff --git a/crates/macros/cgp-macro-core/src/types/cgp_component/args/component_args.rs b/crates/macros/cgp-macro-core/src/types/cgp_component/args/component_args.rs index 17a355af..ad9f763b 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_component/args/component_args.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_component/args/component_args.rs @@ -4,13 +4,13 @@ use syn::{Error, Ident}; use crate::types::attributes::DeriveDelegateAttributes; use crate::types::cgp_component::CgpComponentRawArgs; -use crate::types::ident::NewIdentWithTypeGenerics; +use crate::types::ident::IdentWithTypeGenerics; #[derive(Clone)] pub struct CgpComponentArgs { pub context_ident: Ident, pub provider_ident: Ident, - pub component_name: NewIdentWithTypeGenerics, + pub component_name: IdentWithTypeGenerics, pub derive_delegate_attributes: DeriveDelegateAttributes, } @@ -35,7 +35,7 @@ impl TryFrom for CgpComponentArgs { .unwrap_or_else(|| Ident::new("__Context__", Span::call_site())); let component_name = raw_args.component_name.unwrap_or_else(|| { - NewIdentWithTypeGenerics::from(Ident::new( + IdentWithTypeGenerics::from(Ident::new( &format!("{provider_ident}Component"), Span::call_site(), )) diff --git a/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs b/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs index 2d961618..1a87f01b 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_component/args/raw.rs @@ -3,13 +3,13 @@ use syn::token::{Colon, Comma}; use syn::{Error, Ident}; use crate::types::attributes::DeriveDelegateAttributes; -use crate::types::ident::NewIdentWithTypeGenerics; +use crate::types::ident::IdentWithTypeGenerics; #[derive(Default)] pub struct CgpComponentRawArgs { pub context_ident: Option, pub provider_ident: Option, - pub component_name: Option, + pub component_name: Option, pub derive_delegate_attributes: Option, } 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 e32877dc..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::{NewIdentWithTypeGenerics, PathWithTypeArgs}; +use crate::types::ident::{IdentWithTypeGenerics, PathWithTypeArgs}; use crate::types::provider_impl::ItemProviderImpl; pub struct ItemCgpProvider { @@ -57,7 +57,7 @@ impl ItemCgpProvider { let impl_self_type = &provider_impl.self_ty; - let provider_type: NewIdentWithTypeGenerics = parse_internal!( #impl_self_type ); + let provider_type: IdentWithTypeGenerics = parse_internal!( #impl_self_type ); let provider_struct = EmptyStruct { ident: provider_type.ident.clone(), 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 f986ea9f..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 @@ -8,7 +8,7 @@ use crate::traits::ParseOptionalKeyword; use crate::types::delegate_component::{DelegateEntries, ExtractInnerDelegateTables}; use crate::types::empty_struct::EmptyStruct; use crate::types::generics::ImplGenerics; -use crate::types::ident::NewIdentWithTypeGenerics; +use crate::types::ident::IdentWithTypeGenerics; use crate::types::keyword::Keyword; use crate::types::keywords::New; @@ -54,7 +54,7 @@ impl DelegateTable { let mut item_structs = Vec::new(); if self.new.is_some() { - let struct_type: NewIdentWithTypeGenerics = + let struct_type: IdentWithTypeGenerics = parse_internal(self.table_type.to_token_stream())?; item_structs.push(EmptyStruct { ident: struct_type.ident, 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..801c6005 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,25 @@ 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>`. +/// +/// This is the intended replacement for `IdentWithTypeArgs`. The difference is +/// that the argument list is modelled by [`TypeArgs`] rather than +/// `syn::AngleBracketedGenericArguments`, so invalid forms such as +/// `Foo` are rejected at parse time. +/// +/// 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,11 +44,17 @@ 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) 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..34bf4948 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,21 @@ 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>`. +/// +/// This is the intended replacement for `IdentWithTypeGenerics`. The difference +/// is that the parameter list is modelled by [`TypeGenericParams`] rather than +/// `syn::Generics`, so only simple, unconstrained parameters are accepted. +/// Invalid forms such as `Foo` (bounds) and `Foo` (defaults) +/// are rejected at parse time, without the `split_for_impl` round-trip hack +/// used by the original `TypeGenerics`. #[derive(Debug, Clone)] pub struct IdentWithTypeGenerics { pub ident: Ident, - pub type_generics: TypeGenerics, + pub type_generics: TypeGenericParams, } impl IdentWithTypeGenerics { @@ -21,7 +30,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 2ab4fcee..b2bc8341 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/mod.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/mod.rs @@ -1,11 +1,11 @@ -mod new_ident_with_type_args; -mod new_ident_with_type_generics; +mod ident_with_type_args; +mod ident_with_type_generics; mod path_with_type_args; mod type_arg; mod type_generic_param; -pub use new_ident_with_type_args::*; -pub use new_ident_with_type_generics::*; +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/new_ident_with_type_args.rs b/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_args.rs deleted file mode 100644 index e3525b4a..00000000 --- a/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_args.rs +++ /dev/null @@ -1,62 +0,0 @@ -use proc_macro2::TokenStream; -use quote::ToTokens; -use syn::parse::{Parse, ParseStream}; -use syn::{Ident, Type, parse_quote}; - -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>`. -/// -/// This is the intended replacement for `IdentWithTypeArgs`. The difference is -/// that the argument list is modelled by [`TypeArgs`] rather than -/// `syn::AngleBracketedGenericArguments`, so invalid forms such as -/// `Foo` are rejected at parse time. -/// -/// For the path-headed counterpart (`path::to::Foo`), see -/// [`PathWithTypeArgs`]. -/// -/// [`PathWithTypeArgs`]: crate::types::ident::PathWithTypeArgs -#[derive(Debug, Clone)] -pub struct NewIdentWithTypeArgs { - pub ident: Ident, - pub type_args: TypeArgs, -} - -impl Parse for NewIdentWithTypeArgs { - fn parse(input: ParseStream) -> syn::Result { - let ident = input.parse()?; - let type_args = input.parse()?; - - Ok(Self { ident, type_args }) - } -} - -impl ToTokens for NewIdentWithTypeArgs { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.ident.to_tokens(tokens); - self.type_args.to_tokens(tokens); - } -} - -impl From for NewIdentWithTypeArgs { - fn from(ident: Ident) -> Self { - Self { - ident, - type_args: TypeArgs::default(), - } - } -} - -impl ToType for NewIdentWithTypeArgs { - fn to_type(&self) -> Type { - parse_quote!(#self) - } -} - -impl From for Type { - fn from(value: NewIdentWithTypeArgs) -> Self { - parse_quote!(#value) - } -} diff --git a/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs b/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs deleted file mode 100644 index 8eb96b01..00000000 --- a/crates/macros/cgp-macro-core/src/types/ident/new_ident_with_type_generics.rs +++ /dev/null @@ -1,55 +0,0 @@ -use proc_macro2::TokenStream; -use quote::ToTokens; -use syn::parse::{Parse, ParseStream}; -use syn::{Ident, Type, parse_quote}; - -use crate::types::ident::TypeGenericParams; - -/// An identifier followed by an optional definition-site generic parameter -/// list, e.g. `Foo`, `Foo`, or `Bar<'a, C>`. -/// -/// This is the intended replacement for `IdentWithTypeGenerics`. The difference -/// is that the parameter list is modelled by [`TypeGenericParams`] rather than -/// `syn::Generics`, so only simple, unconstrained parameters are accepted. -/// Invalid forms such as `Foo` (bounds) and `Foo` (defaults) -/// are rejected at parse time, without the `split_for_impl` round-trip hack -/// used by the original `TypeGenerics`. -#[derive(Debug, Clone)] -pub struct NewIdentWithTypeGenerics { - pub ident: Ident, - pub type_generics: TypeGenericParams, -} - -impl NewIdentWithTypeGenerics { - pub fn to_type(&self) -> Type { - parse_quote!(#self) - } -} - -impl From for NewIdentWithTypeGenerics { - fn from(ident: Ident) -> Self { - Self { - ident, - type_generics: TypeGenericParams::default(), - } - } -} - -impl Parse for NewIdentWithTypeGenerics { - fn parse(input: ParseStream) -> syn::Result { - let ident = input.parse()?; - let type_generics = input.parse()?; - - Ok(Self { - ident, - type_generics, - }) - } -} - -impl ToTokens for NewIdentWithTypeGenerics { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.ident.to_tokens(tokens); - self.type_generics.to_tokens(tokens); - } -} 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 index cb1ffd2e..8db5ddab 100644 --- 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 @@ -5,7 +5,7 @@ use syn::punctuated::Punctuated; use syn::{Error, Ident, Path, PathArguments, Type, parse_quote}; use crate::traits::ToType; -use crate::types::ident::{NewIdentWithTypeArgs, TypeArg, TypeArgs}; +use crate::types::ident::{IdentWithTypeArgs, TypeArg, 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>`. @@ -114,8 +114,8 @@ impl From for PathWithTypeArgs { } } -impl From for PathWithTypeArgs { - fn from(value: NewIdentWithTypeArgs) -> Self { +impl From for PathWithTypeArgs { + fn from(value: IdentWithTypeArgs) -> Self { Self { path: Path::from(value.ident), type_args: value.type_args, 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 c88bb46b..055adf42 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::{NewIdentWithTypeGenerics, PathWithTypeArgs}; +use crate::types::ident::{IdentWithTypeGenerics, PathWithTypeArgs}; use crate::types::keyword::Keyword; use crate::types::keywords::New; use crate::types::namespace::{EvaluatedNamespaceTable, InheritNamespaceStatement}; @@ -16,7 +16,7 @@ use crate::types::namespace::{EvaluatedNamespaceTable, InheritNamespaceStatement pub struct NamespaceTable { pub impl_generics: ImplGenerics, pub new: Option>, - pub namespace: NewIdentWithTypeGenerics, + pub namespace: IdentWithTypeGenerics, pub parent_namespace: Option<(Colon, PathWithTypeArgs)>, pub entries: DelegateEntries, } diff --git a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs index 8bfa43b4..4d375d6f 100644 --- a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs +++ b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_inherit.rs @@ -1,5 +1,5 @@ use cgp_macro_core::types::generics::TypeGenerics; -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use proc_macro2::TokenStream; use quote::quote; use syn::{Ident, ItemImpl, ItemStruct, parse_quote, parse2}; @@ -7,7 +7,7 @@ use syn::{Ident, ItemImpl, ItemStruct, parse_quote, parse2}; pub fn cgp_inherit(attr: TokenStream, body: TokenStream) -> syn::Result { let context_struct: ItemStruct = parse2(body)?; - let preset: NewIdentWithTypeArgs = parse2(attr)?; + let preset: IdentWithTypeArgs = parse2(attr)?; let type_generics = TypeGenerics::try_from(&context_struct.generics)?; @@ -26,7 +26,7 @@ pub fn cgp_inherit(attr: TokenStream, body: TokenStream) -> syn::Result, - preset: &NewIdentWithTypeArgs, + preset: &IdentWithTypeArgs, ) -> syn::Result<(ItemImpl, ItemImpl)> { let preset_name = &preset.ident; let preset_generics = &preset.type_args; diff --git a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs index 39dc3261..089f78e5 100644 --- a/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs +++ b/crates/macros/cgp-macro-lib/src/entrypoints/cgp_preset.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use cgp_macro_core::functions::to_snake_case_str; use cgp_macro_core::types::empty_struct::EmptyStruct; use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, TokenStreamExt, quote}; use syn::punctuated::Punctuated; @@ -17,7 +17,7 @@ use crate::preset::{define_substitution_macro, impl_components_is_preset}; pub fn define_preset(body: TokenStream) -> syn::Result { let ast: DefinePreset = syn::parse2(body)?; - let delegate_entries: Punctuated, Comma> = ast + let delegate_entries: Punctuated, Comma> = ast .delegate_entries .iter() .map(|entry| entry.entry.clone()) diff --git a/crates/macros/cgp-macro-lib/src/for_each_replace.rs b/crates/macros/cgp-macro-lib/src/for_each_replace.rs index 957a46ef..6133e13f 100644 --- a/crates/macros/cgp-macro-lib/src/for_each_replace.rs +++ b/crates/macros/cgp-macro-lib/src/for_each_replace.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use proc_macro2::{Group, TokenStream, TokenTree}; use quote::ToTokens; use syn::__private::parse_brackets; @@ -20,10 +20,10 @@ pub struct ReplaceSpecs { impl Parse for ReplaceSpecs { fn parse(input: ParseStream) -> syn::Result { - let raw_replacements: Vec> = { + let raw_replacements: Vec> = { let content = parse_brackets(input)?.content; let types = - , Comma>>::parse_terminated(&content)?; + , Comma>>::parse_terminated(&content)?; types.into_iter().collect() }; diff --git a/crates/macros/cgp-macro-lib/src/parse/check_components.rs b/crates/macros/cgp-macro-lib/src/parse/check_components.rs index 1be8b8a1..924d091f 100644 --- a/crates/macros/cgp-macro-lib/src/parse/check_components.rs +++ b/crates/macros/cgp-macro-lib/src/parse/check_components.rs @@ -1,5 +1,5 @@ use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use proc_macro2::Span; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; @@ -96,7 +96,7 @@ impl Parse for CheckComponents { let trait_name = if let Some(check_trait_name) = m_check_trait_name { check_trait_name } else { - let context_type: NewIdentWithTypeArgs = parse2(context_type.to_token_stream())?; + let context_type: IdentWithTypeArgs = parse2(context_type.to_token_stream())?; Ident::new( &format!("__Check{}", context_type.ident), diff --git a/crates/macros/cgp-macro-lib/src/parse/define_preset.rs b/crates/macros/cgp-macro-lib/src/parse/define_preset.rs index d06a5479..3de9e30d 100644 --- a/crates/macros/cgp-macro-lib/src/parse/define_preset.rs +++ b/crates/macros/cgp-macro-lib/src/parse/define_preset.rs @@ -1,4 +1,4 @@ -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use proc_macro2::TokenStream; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; @@ -17,7 +17,7 @@ pub struct DefinePreset { pub struct DelegatePresetEntry { pub is_override: Option, - pub entry: DelegateEntry, + pub entry: DelegateEntry, } impl Parse for DefinePreset { @@ -82,7 +82,7 @@ impl Parse for DelegatePresetEntry { #[derive(Clone)] pub struct PresetParent { pub has_expanded: Option, - pub parent_type: NewIdentWithTypeArgs, + pub parent_type: IdentWithTypeArgs, } impl Parse for PresetParent { diff --git a/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs b/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs index b71b7991..0c11087d 100644 --- a/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs +++ b/crates/macros/cgp-macro-lib/src/parse/delegate_and_check_components.rs @@ -1,7 +1,7 @@ use core::iter; use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; @@ -46,7 +46,7 @@ impl Parse for DelegateAndCheckSpec { let trait_name = match m_trait_name { Some(ident) => ident, None => { - let context_type: NewIdentWithTypeArgs = parse2(context_type.to_token_stream())?; + let context_type: IdentWithTypeArgs = parse2(context_type.to_token_stream())?; Ident::new( &format!("__CanUse{}", context_type.ident), context_type.span(), diff --git a/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs b/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs index 5ff41b62..736aaf1d 100644 --- a/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs +++ b/crates/macros/cgp-macro-lib/src/parse/delegate_components.rs @@ -2,7 +2,7 @@ use core::iter; use cgp_macro_core::functions::merge_generics; use cgp_macro_core::types::generics::{ImplGenerics, TypeGenerics}; -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; use syn::parse::discouraged::Speculative; @@ -129,14 +129,14 @@ impl Parse for DelegateEntry { } } -impl Parse for DelegateEntry { +impl Parse for DelegateEntry { fn parse(input: ParseStream) -> syn::Result { let components = if input.peek(Bracket) { let components_body; bracketed!(components_body in input); components_body.parse_terminated(DelegateKey::parse, Token![,])? } else { - let component: DelegateKey = input.parse()?; + let component: DelegateKey = input.parse()?; Punctuated::from_iter(iter::once(component)) }; diff --git a/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs b/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs index 6cac27eb..72336da1 100644 --- a/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs +++ b/crates/macros/cgp-macro-lib/src/preset/impl_is_preset.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use cgp_macro_core::types::generics::ImplGenerics; -use cgp_macro_core::types::ident::NewIdentWithTypeArgs; +use cgp_macro_core::types::ident::IdentWithTypeArgs; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{Ident, ItemImpl, Type, parse_quote}; @@ -12,7 +12,7 @@ pub fn impl_components_is_preset( trait_name: &Ident, preset_type: &Type, preset_generics: &ImplGenerics, - delegate_entries: &Punctuated, Comma>, + delegate_entries: &Punctuated, Comma>, ) -> Vec { delegate_entries .iter() @@ -28,7 +28,7 @@ pub fn impl_component_is_preset( trait_name: &Ident, _preset_type: &Type, _preset_generics: &ImplGenerics, - component: &DelegateKey, + component: &DelegateKey, ) -> ItemImpl { let component_type = &component.ty; diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs index 9b23e080..e9e5679f 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs @@ -2,8 +2,8 @@ //! constructs in `cgp-macro-core`. //! //! These cover both the original constructs (`IdentWithTypeArgs`, -//! `IdentWithTypeGenerics`) and the new ones (`NewIdentWithTypeArgs`, -//! `NewIdentWithTypeGenerics`, `PathWithTypeArgs`), including a side-by-side +//! `IdentWithTypeGenerics`) and the new ones (`IdentWithTypeArgs`, +//! `IdentWithTypeGenerics`, `PathWithTypeArgs`), including a side-by-side //! comparison that documents exactly where their behaviors diverge. //! //! See `crates/macros/cgp-macro-core/src/types/ident/README.md` for the prose diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs index 55f8b649..f22bcb06 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs @@ -1,13 +1,13 @@ -//! Corner cases for `NewIdentWithTypeArgs` — an identifier followed by an +//! Corner cases for `IdentWithTypeArgs` — an identifier followed by an //! optional *type-expression* argument list, e.g. `Foo`. -use cgp_macro_core::types::ident::{NewIdentWithTypeArgs, TypeArg}; +use cgp_macro_core::types::ident::{IdentWithTypeArgs, TypeArg}; use quote::quote; use syn::parse2; use super::{assert_idempotent, assert_parses, assert_rejects}; -type Subject = NewIdentWithTypeArgs; +type Subject = IdentWithTypeArgs; #[test] fn accepts_bare_ident() { diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs index bb60a3e7..45ef5fcb 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs @@ -1,14 +1,14 @@ -//! Corner cases for `NewIdentWithTypeGenerics` — an identifier followed by an +//! 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::{NewIdentWithTypeGenerics, TypeGenericParam}; +use cgp_macro_core::types::ident::{IdentWithTypeGenerics, TypeGenericParam}; use quote::quote; use syn::parse2; use super::{assert_idempotent, assert_parses, assert_rejects}; -type Subject = NewIdentWithTypeGenerics; +type Subject = IdentWithTypeGenerics; #[test] fn accepts_bare_ident() { From f54502ab9cd2a5093b1d2cc18640e868f24f1329 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:25:12 +0200 Subject: [PATCH 07/15] Remove old references --- .../cgp-macro-core/src/types/ident/README.md | 371 ------------------ .../src/types/ident/ident_with_type_args.rs | 5 - .../types/ident/ident_with_type_generics.rs | 7 - .../src/types/ident/path_with_type_args.rs | 4 +- .../cgp-macro-lib/src/parse/component_spec.rs | 106 ----- 5 files changed, 2 insertions(+), 491 deletions(-) delete mode 100644 crates/macros/cgp-macro-core/src/types/ident/README.md delete mode 100644 crates/macros/cgp-macro-lib/src/parse/component_spec.rs diff --git a/crates/macros/cgp-macro-core/src/types/ident/README.md b/crates/macros/cgp-macro-core/src/types/ident/README.md deleted file mode 100644 index 7371c7bb..00000000 --- a/crates/macros/cgp-macro-core/src/types/ident/README.md +++ /dev/null @@ -1,371 +0,0 @@ -# Identifiers and Paths with Type Parameters - -This module provides the parsing constructs that CGP macros use to read -identifiers and paths that carry type parameters, such as `Foo`, -`Bar<'a, C>`, or `path::to::Foo<(A, B), C>`. - -There are two fundamentally different *positions* in which type parameters -appear, and CGP needs to parse both. Getting the distinction right matters, -because the two positions allow different syntax: - -- **Definition site** — where generic parameters are *introduced*, e.g. the - `` in `struct Foo` or the `<'a, C>` in `trait Bar<'a, C>`. Here - each parameter is a *simple, unconstrained binder*: a lifetime, a type - identifier, or a const parameter. -- **Type-expression site** — where generic arguments are *applied*, e.g. the - `` in `Foo` used as a type. Here each argument may be an arbitrary - type, including composite forms such as `(A, B)` or `Bar`. - -The constructs in this module fall into these two groups, plus the supporting -argument/parameter types they are built from. - -| Construct | Position | Head | Status | -|---|---|---|---| -| `IdentWithTypeGenerics` | definition site | `Ident` | original | -| `IdentWithTypeArgs` | type-expression site | `Ident` | original | -| `NewIdentWithTypeGenerics` | definition site | `Ident` | new | -| `NewIdentWithTypeArgs` | type-expression site | `Ident` | new | -| `PathWithTypeArgs` | type-expression site | `syn::Path` | new | - -The behavior described here is exercised by the test suite at -`crates/tests/cgp-tests/tests/ident_with_type_params_tests/`. - ---- - -## Why `syn`'s constructs are not sufficient - -CGP needs *faithful* parsers: parsers that accept exactly the forms that are -valid in a given position, and reject everything else at parse time with a clear -error. The off-the-shelf `syn` constructs do not give us this, for three -different reasons. - -### 1. `syn::AngleBracketedGenericArguments` is too permissive for arguments - -The original `IdentWithTypeArgs` models its argument list with -`syn::AngleBracketedGenericArguments`, whose elements are -[`syn::GenericArgument`]: - -```rust -pub enum GenericArgument { - Lifetime(Lifetime), // 'a — valid here - Type(Type), // A, (A, B) — valid here - Const(Expr), // 3, { N } — valid here - AssocType(AssocType), // Item = T — NOT valid in a type-argument position - AssocConst(AssocConst), // N = 1 — NOT valid in a type-argument position - Constraint(Constraint), // Item: Clone — NOT valid in a type-argument position -} -``` - -The last three variants are only meaningful inside *trait bounds* (e.g. -`dyn Iterator`), not in a plain applied type like a struct field type. -But `AngleBracketedGenericArguments` accepts all six variants unconditionally, so -`IdentWithTypeArgs` silently accepts invalid input such as `Foo` or -`Foo`. - -### 2. `syn::Generics` is the wrong shape for definition-site parameters - -The original `IdentWithTypeGenerics` models its parameter list with -`syn::Generics`. But `Generics` is designed for full `impl`/`struct`/`fn` -headers: every parameter can carry bounds and a default, and the whole thing can -carry a trailing `where` clause. None of that is allowed in the simple -definition-site lists CGP cares about. - -To compensate, the original `TypeGenerics` parser uses a *round-trip hack*: it -parses a full `Generics`, runs it through `split_for_impl()` to obtain the -"type generics" projection (which strips bounds and defaults), re-parses that, -and rejects the input if the two differ: - -```rust -let generics: Generics = input.parse()?; -let (_, type_generics, _) = generics.split_for_impl(); -let generics2: Generics = parse_internal(type_generics.to_token_stream())?; -if generics != generics2 { - return Err(Error::new_spanned(generics, "invalid type generics syntax")); -} -``` - -This works for rejecting bounds and defaults, but it is opaque, it relies on a -structural-equality comparison as a validation mechanism, and it has a surprising -side effect: it rejects **const generics**. The type-generics projection of -`const N: usize` is the bare `N`, which re-parses as a *type* parameter, so the -equality check fails and `Bar` is rejected — even though const -parameters are perfectly valid at a definition site. - -### 3. `syn::Path` buries the final arguments - -CGP frequently parses things that are genuinely Rust *paths* — provider trait -paths, namespace references, preset names — many of which are pulled out of a -real `syn::Path` (for instance from `item_impl.trait_`). The arguments we care -about live on the **final segment**, hidden inside -`path.segments.last().arguments` as a `PathArguments` enum. Reaching them, and -rewriting them (e.g. inserting the `Context` type as the first argument), is -awkward and repetitive. - -Because the original `IdentWithTypeArgs` head is a single `Ident`, it cannot -parse a path at all: `path::to::Foo` is rejected outright. So any site that -wanted to accept a qualified path had no construct to use. - ---- - -## The constructs - -### Supporting types - -#### `TypeArg` / `TypeArgs` (type-expression arguments) - -`TypeArg` is the faithful element of a type-argument list. It is the restriction -of `syn::GenericArgument` to the three valid variants: - -```rust -pub enum TypeArg { - Lifetime(Lifetime), // 'a - Type(Type), // A, (A, B), Bar, &'a A, [A; 4], dyn Tr, ... - Const(Expr), // a literal (3, true) or a braced block ({ N }) -} -``` - -Associated bindings (`Item = T`), associated const bindings (`N = 1`), and -associated bounds (`Item: Clone`) are rejected during parsing. `TypeArg` can -also be produced from an already-parsed `syn::GenericArgument` via -`TypeArg::from_generic_argument`, which applies the same validation (used by -`PathWithTypeArgs`). - -`TypeArgs` is the optional angle-bracketed list: - -```rust -pub struct TypeArgs { - pub args: Option>, -} -``` - -`None` means there were no angle brackets at all (`Foo`); `Some(empty)` means an -explicit empty list (`Foo<>`). `make_args(&mut self)` returns the inner -`Punctuated`, inserting an empty list if necessary — this mirrors the original -`GenericArguments::make_args` so migration is mechanical. - -#### `TypeGenericParam` / `TypeGenericParams` (definition-site parameters) - -`TypeGenericParam` is the faithful element of a definition-site parameter list: - -```rust -pub enum TypeGenericParam { - Lifetime(Lifetime), // 'a - Type(Ident), // C - Const(ConstGenericParam), // const N: usize -} -``` - -Each variant is a *bare* binder: bounds (`A: Clone`, `'a: 'b`) and defaults -(`A = B`, `const N: usize = 0`) are rejected, as are composite forms -(`(A, B)`, `Bar`). `ConstGenericParam` deliberately has no default field. - -`TypeGenericParams` is the optional list, with the same `None` / `Some(empty)` -convention as `TypeArgs` and a `make_params` accessor. It also provides -`to_generics()`, which lowers the parameters into a plain `syn::Generics` for -downstream code that builds struct/impl headers (e.g. `EmptyStruct`). - -### Original constructs - -#### `IdentWithTypeGenerics` = `Ident` + `TypeGenerics` - -A definition-site construct. Accepts `Foo`, `Foo`, `Bar<'a, C>`. Rejects -bounds, defaults, and composite parameters via the `split_for_impl` round-trip -hack — and, as a side effect of that hack, also rejects const parameters -(`Bar`). - -#### `IdentWithTypeArgs` = `Ident` + `GenericArguments` - -A type-expression construct. Accepts `Foo`, `Foo`, composite arguments -(`Foo<(A, B), Bar>`), lifetimes, and const arguments. **Unfaithfully** also -accepts associated bindings and bounds (`Foo`, `Foo`, -`Foo`, `Foo`). The head is a single `Ident`, so paths -(`path::to::Foo`) and turbofish (`Foo::`) are rejected. - -### New constructs - -#### `NewIdentWithTypeGenerics` = `Ident` + `TypeGenericParams` - -The intended replacement for `IdentWithTypeGenerics`. Same accepted forms, but: - -- the parameter list is modelled directly, with no round-trip hack; -- **const parameters are accepted** (`Bar`); -- defaults are rejected up front (the original happens to reject them too, but - only as a side effect of the structural comparison). - -#### `NewIdentWithTypeArgs` = `Ident` + `TypeArgs` - -The intended replacement for `IdentWithTypeArgs`. Same valid forms, but -associated bindings/bounds are rejected at parse time. The head is still a single -`Ident` (paths and turbofish rejected). - -#### `PathWithTypeArgs` = `syn::Path` + `TypeArgs` - -A generalization of `NewIdentWithTypeArgs` from a single-identifier head to a -full path head. It parses a `syn::Path`, then **lifts the final segment's -arguments** out into a separate `type_args: TypeArgs` field, leaving `path` free -of those arguments. This makes the final arguments directly accessible and -rewritable, while `ident()` exposes the final segment's identifier. - -Restrictions enforced during parsing: - -- only the final segment may carry arguments — `path::to::Foo` is rejected; -- turbofish is rejected — `path::to::Foo::` must be written `path::to::Foo`; -- parenthesized arguments are rejected — `Fn(A) -> B`; -- arguments obey the same `TypeArg` rules (no associated bindings/bounds). - -Because any single identifier is also a valid one-segment path, every input that -`NewIdentWithTypeArgs` accepts is also accepted by `PathWithTypeArgs`. -`PathWithTypeArgs` is therefore a strict superset, and is the right default at a -type-expression site unless a qualified path must specifically be forbidden. - ---- - -## Behavior comparison - -### Type-expression position (`IdentWithTypeArgs` vs `NewIdentWithTypeArgs` / `PathWithTypeArgs`) - -| Input | `IdentWithTypeArgs` | `NewIdentWithTypeArgs` | `PathWithTypeArgs` | -|---|---|---|---| -| `Foo` | accept | accept | accept | -| `Foo<>` | accept | accept | accept | -| `Foo` | accept | accept | accept | -| `Foo<(A, B), C>` | accept | accept | accept | -| `Foo, C>` | accept | accept | accept | -| `Foo<'a, A>` | accept | accept | accept | -| `Foo<3>`, `Foo<{ N }>` | accept | accept | accept | -| `Foo` | **accept** ⚠ | reject | reject | -| `Foo` | **accept** ⚠ | reject | reject | -| `Foo` | **accept** ⚠ | reject | reject | -| `Foo` | **accept** ⚠ | reject | reject | -| `path::to::Foo` | reject | reject | **accept** | -| `Foo::` (turbofish) | reject | reject | reject | -| `Fn(A) -> B` | reject | reject | reject | - -⚠ marks the original construct's unfaithful acceptances. - -### Definition-site position (`IdentWithTypeGenerics` vs `NewIdentWithTypeGenerics`) - -| Input | `IdentWithTypeGenerics` | `NewIdentWithTypeGenerics` | -|---|---|---| -| `Foo` | accept | accept | -| `Foo` | accept | accept | -| `Bar<'a, C>` | accept | accept | -| `Bar` | **reject** | **accept** | -| `Foo` | reject | reject | -| `Foo<'a: 'b>` | reject | reject | -| `Foo` | reject | reject | -| `Foo<(A, B)>` | reject | reject | -| `Foo>` | reject | reject | -| `path::to::Foo` | reject | reject | - ---- - -## Forms of generic parameters considered - -For completeness, here is the full matrix of generic-parameter/argument forms in -the Rust grammar and how the new constructs treat each. - -| Form | Example | Def site (`NewIdentWithTypeGenerics`) | Arg site (`NewIdentWithTypeArgs` / `PathWithTypeArgs`) | -|---|---|---|---| -| Lifetime | `'a` | accept | accept | -| Type identifier | `A` | accept | accept (as a `Type`) | -| Composite type | `(A, B)`, `Bar`, `&'a A`, `[A; 4]`, `dyn Tr`, `fn(A) -> B` | reject | accept | -| Const parameter | `const N: usize` | accept | n/a | -| Const argument | `3`, `true`, `{ N }` | n/a | accept | -| Trait/lifetime bound | `A: Clone`, `'a: 'b` | reject | reject | -| Default | `A = B`, `const N: usize = 0` | reject | n/a | -| Associated type binding | `Item = T` | n/a | reject | -| Associated const binding | `N = 1` | n/a | reject | -| Associated type bound | `Item: Clone` | n/a | reject | -| Where clause | `where A: Clone` | not consumed (out of scope) | not consumed | -| Turbofish | `::` | n/a | reject | -| Path head | `path::to::Foo` | reject | `PathWithTypeArgs` only | -| Intermediate-segment args | `path::to::Foo` | n/a | reject | -| Parenthesized args | `Fn(A) -> B` | n/a | reject | - -Two design decisions worth highlighting: - -- **Const generics are supported.** CGP does not appear to use them today, but - they are a legitimate Rust form, so the new constructs accept them rather than - rejecting them by accident (as the original definition-site construct does). -- **A bare-identifier const argument is parsed as a `Type`.** `Foo` is - classified as `TypeArg::Type`, not `TypeArg::Const`, exactly as `syn` does, - because the two are syntactically indistinguishable without resolution. Only - literal or braced const arguments are classified as `Const`. - ---- - -## Migration guide - -The replacements come in two flavors: - -- **Definition sites** → `NewIdentWithTypeGenerics` (a drop-in for - `IdentWithTypeGenerics`). -- **Type-expression sites** → `PathWithTypeArgs` by default, or - `NewIdentWithTypeArgs` where a qualified path must be forbidden. - -The original public surface is mirrored on the new types (`From`, -`ToTokens`, `to_type`/`Into`, `make_args`/`make_params`), so most call -sites change only the type name. The notable structural changes are: argument -iteration now yields `TypeArg` instead of `syn::GenericArgument`, and the path -head is reached through `ident()` / `path` rather than a bare `ident` field. - -### Definition sites → `NewIdentWithTypeGenerics` - -These all introduce a fresh local name, so an ident head is correct. - -| Location | Field | -|---|---| -| `types/cgp_component/args/component_args.rs`, `args/raw.rs` | `component_name` | -| `types/attributes/prefix.rs` | `component_name` parameter | -| `types/namespace/table.rs` | `namespace` (table head, possibly `new`) | -| `types/delegate_component/table/main.rs` | `struct_type` (the `new` table struct) | -| `types/cgp_provider/item.rs` | `provider_type` (the provider struct being defined) | -| `cgp-macro-lib/parse/component_spec.rs` | `component_name` | - -Downstream code that consumes `.type_generics.generics` (a `syn::Generics`) -should call `.type_generics.to_generics()` instead. - -### Type-expression sites → `PathWithTypeArgs` - -These name an existing item (a trait, namespace, preset, or component-name type) -that may legitimately be written as a qualified path. Several already extract -their value from a real `syn::Path`, so moving to `PathWithTypeArgs` both fixes -the faithfulness gap *and* unlocks the `path::to::Foo` use case. - -| Location | Field / use | Notes | -|---|---|---| -| `types/cgp_provider/item.rs` | `provider_trait` | parsed from `item_impl.trait_`; use `.ident()` to build `{Trait}Component` | -| `types/provider_impl.rs` | `provider_path` destructuring | replace `IdentWithTypeArgs { ident, type_args }` with `path`/`type_args` + `ident()` | -| `types/cgp_impl/lowered.rs` | `provider_trait_path` | `make_args().insert(0, ..)` still works (now over `TypeArg`) | -| `types/attributes/use_type/attribute.rs` | `trait_path` | already named a "path" | -| `types/attributes/use_provider/attribute.rs` | `provider_trait_bounds` | trait names may be paths | -| `types/attributes/uses.rs`, `attributes/function.rs`, `attributes/cgp_impl_attributes.rs` | `uses` / `imports` | `#[uses(Trait<..>)]` trait names | -| `types/attributes/prefix.rs`, `attributes/default_impl/attribute.rs` | `namespace` | namespace references | -| `types/namespace/table.rs` | `parent_namespace` | | -| `types/namespace/inherit.rs` | `namespace` | | -| `types/delegate_component/statement/for_loop.rs`, `statement/eval.rs` | `namespace` | | -| `cgp-macro-lib/entrypoints/cgp_inherit.rs` | `preset` | preset names may be paths | -| `cgp-macro-lib/parse/define_preset.rs` | `parent_type`, preset entries | | -| `cgp-macro-lib/entrypoints/cgp_preset.rs`, `preset/impl_is_preset.rs`, `for_each_replace.rs` | `DelegateEntry`/`DelegateKey` | component-name keys | -| `cgp-macro-lib/parse/delegate_components.rs` | `DelegateEntry` | preset form | -| `cgp-macro-lib/parse/check_components.rs` | `context_type` for naming | only `.ident()` is needed | - -### Type-expression sites → `NewIdentWithTypeArgs` - -Use this only where the grammar should specifically forbid a qualified path — -for example a freshly declared local name that nonetheless appears in -argument-list syntax. In practice the "must be a single ident" cases in CGP are -all *definition* sites (covered by `NewIdentWithTypeGenerics`), so -`NewIdentWithTypeArgs` is mostly useful as the precise, path-rejecting option -when reviewing a specific site shows that a path would be meaningless there. - -### `ProviderImplArgs::from_generic_args` - -`types/cgp_provider/provider_impl_args.rs` currently iterates -`syn::GenericArgument` to split the leading `Context` type from the remaining -impl arguments. After migration it should take a `&TypeArgs` (or -`&Punctuated`) and match on `TypeArg::{Lifetime, Type, Const}` — -which is a more direct mapping than the current `GenericArgument` match (the -`_ => Err(..)` arm for unsupported variants disappears, since `TypeArg` has no -invalid variants). 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 801c6005..117fb654 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 @@ -9,11 +9,6 @@ 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>`. /// -/// This is the intended replacement for `IdentWithTypeArgs`. The difference is -/// that the argument list is modelled by [`TypeArgs`] rather than -/// `syn::AngleBracketedGenericArguments`, so invalid forms such as -/// `Foo` are rejected at parse time. -/// /// For the path-headed counterpart (`path::to::Foo`), see /// [`PathWithTypeArgs`]. /// 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 34bf4948..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 @@ -7,13 +7,6 @@ use crate::types::ident::TypeGenericParams; /// An identifier followed by an optional definition-site generic parameter /// list, e.g. `Foo`, `Foo`, or `Bar<'a, C>`. -/// -/// This is the intended replacement for `IdentWithTypeGenerics`. The difference -/// is that the parameter list is modelled by [`TypeGenericParams`] rather than -/// `syn::Generics`, so only simple, unconstrained parameters are accepted. -/// Invalid forms such as `Foo` (bounds) and `Foo` (defaults) -/// are rejected at parse time, without the `split_for_impl` round-trip hack -/// used by the original `TypeGenerics`. #[derive(Debug, Clone)] pub struct IdentWithTypeGenerics { pub ident: Ident, 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 index 8db5ddab..7b6b7855 100644 --- 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 @@ -10,7 +10,7 @@ use crate::types::ident::{IdentWithTypeArgs, TypeArg, 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 [`NewIdentWithTypeArgs`] from a single identifier head to a +/// 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 @@ -62,7 +62,7 @@ impl Parse for PathWithTypeArgs { PathArguments::None => TypeArgs { args: None }, PathArguments::AngleBracketed(arguments) => { // Reject turbofish (`Foo::`); only the type-position form - // `Foo` is accepted, matching `NewIdentWithTypeArgs`. + // `Foo` is accepted, matching `IdentWithTypeArgs`. if arguments.colon2_token.is_some() { return Err(Error::new_spanned( arguments, 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 73acc36b..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::NewIdentWithTypeGenerics; -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: NewIdentWithTypeGenerics, - 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 { - NewIdentWithTypeGenerics::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, - }) - } -} From 5390dacecca5ba79d3e7d6e2433c9a424e444ccf Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:37:51 +0200 Subject: [PATCH 08/15] Remove Option from `TypeGenericParams::params` --- .../src/types/attributes/prefix.rs | 2 +- .../src/types/ident/type_generic_param.rs | 35 +++++++------------ .../src/types/namespace/table.rs | 4 +-- .../new_ident_with_type_args.rs | 10 ------ .../new_ident_with_type_generics.rs | 5 +-- 5 files changed, 16 insertions(+), 40 deletions(-) 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 6dc0a007..76f8bc03 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs @@ -30,7 +30,7 @@ impl PrefixAttribute { let mut type_generics = component_name.type_generics.clone(); type_generics - .make_params() + .params .insert(0, parse_internal!(__Components__)); let item_impl = parse_internal! { 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 index cd954605..971a7950 100644 --- 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 @@ -112,28 +112,20 @@ impl ToTokens for TypeGenericParam { } } -/// The optional angle-bracketed parameter list at a type definition site, e.g. -/// the `<'a, C>` in `Bar<'a, C>`. +/// The angle-bracketed parameter list at a type definition site, e.g. the +/// `<'a, C>` in `Bar<'a, C>`. /// -/// As with [`TypeArgs`], `None` represents no angle brackets while -/// `Some(empty)` represents an explicit empty `<>`. -/// -/// [`TypeArgs`]: crate::types::ident::TypeArgs +/// 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: Option>, + pub params: Punctuated, } impl TypeGenericParams { - pub fn make_params(&mut self) -> &mut Punctuated { - self.params.get_or_insert_with(Punctuated::new) - } - pub fn is_empty(&self) -> bool { - match &self.params { - Some(params) => params.is_empty(), - None => true, - } + self.params.is_empty() } /// Lower these parameters into a plain [`syn::Generics`]. This is handy for @@ -146,14 +138,14 @@ impl TypeGenericParams { impl Parse for TypeGenericParams { fn parse(input: ParseStream) -> syn::Result { + let mut params = Punctuated::new(); + if !input.peek(Lt) { - return Ok(Self { params: None }); + return Ok(Self { params }); } let _: Lt = input.parse()?; - let mut params = Punctuated::new(); - while !input.peek(Gt) { params.push_value(input.parse()?); @@ -166,15 +158,14 @@ impl Parse for TypeGenericParams { let _: Gt = input.parse()?; - Ok(Self { - params: Some(params), - }) + Ok(Self { params }) } } impl ToTokens for TypeGenericParams { fn to_tokens(&self, tokens: &mut TokenStream) { - if let Some(params) = &self.params { + if !self.params.is_empty() { + let params = &self.params; tokens.extend(quote! { < #params > }); } } 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 055adf42..534dd652 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/table.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/table.rs @@ -57,9 +57,7 @@ impl NamespaceTable { pub fn build_namespace_trait(&self) -> syn::Result { let namespace_ident = &self.namespace.ident; let mut namespace_generics = self.namespace.type_generics.clone(); - namespace_generics - .make_params() - .push(parse_internal!(__Table__)); + namespace_generics.params.push(parse_internal!(__Table__)); let namespace_trait: Type = parse_internal!( #namespace_ident #namespace_generics ); Ok(namespace_trait) diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs index f22bcb06..85a800ef 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs @@ -123,16 +123,6 @@ fn bare_ident_has_no_arguments() { assert!(parsed.type_args.is_empty()); } -#[test] -fn empty_brackets_are_distinct_from_no_brackets() { - let bare: Subject = parse2(quote!(Foo)).unwrap(); - let empty: Subject = parse2(quote!(Foo)).unwrap(); - - assert!(bare.type_args.args.is_none()); - assert!(empty.type_args.args.is_some()); - assert!(empty.type_args.is_empty()); -} - #[test] fn round_trips() { assert_idempotent::(quote!(Foo)); diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs index 45ef5fcb..4a0b25a8 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs @@ -75,10 +75,7 @@ fn rejects_path_head() { fn classifies_each_parameter_form() { let parsed: Subject = parse2(quote!(Bar<'a, C, const N: usize>)).unwrap(); - let params = parsed - .type_generics - .params - .expect("expected a parameter list"); + let params = &parsed.type_generics.params; let kinds: Vec<&str> = params .iter() From 33e2ae336a4037381e3cb0ea4be3ffd9586303ae Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:42:41 +0200 Subject: [PATCH 09/15] Remove Option from `TypeArgs::args` --- .../types/cgp_provider/provider_impl_args.rs | 34 +++++++++---------- .../delegate_component/statement/eval.rs | 5 +-- .../src/types/ident/path_with_type_args.rs | 6 ++-- .../src/types/ident/type_arg.rs | 32 ++++++++--------- .../new_ident_with_type_args.rs | 7 ++-- .../path_with_type_args.rs | 4 +-- 6 files changed, 42 insertions(+), 46 deletions(-) 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 fceb551f..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 @@ -22,26 +22,24 @@ impl ProviderImplArgs { let mut impl_args: Punctuated = Punctuated::new(); let mut context_type: Option = None; - if let Some(args) = &generic_args.args { - for arg in 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", - )); + 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 d0a72d5a..d38a4a71 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 @@ -54,10 +54,7 @@ impl EvalDelegateEntry for EvaluatedForEntry { // existing arguments. let namespace_path = &self.namespace.path; - let existing_args: Vec<&TypeArg> = match &self.namespace.type_args.args { - Some(args) => args.iter().collect(), - None => Vec::new(), - }; + let existing_args: Vec<&TypeArg> = self.namespace.type_args.args.iter().collect(); parse_internal! { #namespace_path < #( #existing_args, )* #table_type, Delegate = #mapping_value > 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 index 7b6b7855..c3d29c9d 100644 --- 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 @@ -59,7 +59,9 @@ impl Parse for PathWithTypeArgs { let last_segment = path.segments.last_mut().unwrap(); let type_args = match &last_segment.arguments { - PathArguments::None => TypeArgs { args: None }, + PathArguments::None => TypeArgs { + args: Punctuated::new(), + }, PathArguments::AngleBracketed(arguments) => { // Reject turbofish (`Foo::`); only the type-position form // `Foo` is accepted, matching `IdentWithTypeArgs`. @@ -80,7 +82,7 @@ impl Parse for PathWithTypeArgs { } } - TypeArgs { args: Some(args) } + TypeArgs { args } } PathArguments::Parenthesized(arguments) => { return Err(Error::new_spanned( 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 index bf574b2f..42b7469a 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs @@ -118,38 +118,35 @@ impl ToTokens for TypeArg { } } -/// The optional 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>`. +/// 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>`. /// -/// `None` represents the absence of any angle brackets (the bare `Foo` case), -/// whereas `Some(empty)` represents an explicit empty `Foo<>`. This mirrors the -/// behaviour of the existing `GenericArguments` type so the two can be used -/// interchangeably during migration. +/// 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: Option>, + pub args: Punctuated, } impl TypeArgs { - /// Get a mutable reference to the underlying argument list, inserting an - /// empty `<>` list if none is present yet. This matches the + /// Get a mutable reference to the underlying argument list. This matches the /// `GenericArguments::make_args` API to ease migration. pub fn make_args(&mut self) -> &mut Punctuated { - self.args.get_or_insert_with(Punctuated::new) + &mut self.args } pub fn is_empty(&self) -> bool { - match &self.args { - Some(args) => args.is_empty(), - None => true, - } + self.args.is_empty() } } impl Parse for TypeArgs { fn parse(input: ParseStream) -> syn::Result { if !input.peek(Lt) { - return Ok(Self { args: None }); + return Ok(Self { + args: Punctuated::new(), + }); } let _: Lt = input.parse()?; @@ -168,13 +165,14 @@ impl Parse for TypeArgs { let _: Gt = input.parse()?; - Ok(Self { args: Some(args) }) + Ok(Self { args }) } } impl ToTokens for TypeArgs { fn to_tokens(&self, tokens: &mut TokenStream) { - if let Some(args) = &self.args { + if !self.args.is_empty() { + let args = &self.args; tokens.extend(quote! { < #args > }); } } diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs index 85a800ef..0bb2ca6b 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs @@ -16,7 +16,8 @@ fn accepts_bare_ident() { #[test] fn accepts_empty_argument_list() { - // An explicit empty `<>` is allowed and distinct from no brackets at all. + // An explicit empty `<>` is allowed and parses to an empty argument list, + // indistinguishable from no brackets at all. assert_parses::(quote!(Foo)); } @@ -99,7 +100,7 @@ fn rejects_unterminated_arguments() { 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.expect("expected an argument list"); + let args = &parsed.type_args.args; let kinds: Vec<&str> = args .iter() @@ -119,7 +120,7 @@ fn classifies_each_argument_form() { #[test] fn bare_ident_has_no_arguments() { let parsed: Subject = parse2(quote!(Foo)).unwrap(); - assert!(parsed.type_args.args.is_none()); + assert!(parsed.type_args.args.is_empty()); assert!(parsed.type_args.is_empty()); } diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs index c9e9db85..c6431011 100644 --- a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs +++ b/crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs @@ -79,7 +79,7 @@ fn strips_arguments_from_stored_path() { let last = parsed.path.segments.last().unwrap(); assert!(last.arguments.is_none()); - let args = parsed.type_args.args.expect("expected an argument list"); + let args = &parsed.type_args.args; assert_eq!(args.len(), 2); assert!(matches!(args[0], TypeArg::Type(_))); } @@ -87,7 +87,7 @@ fn strips_arguments_from_stored_path() { #[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_none()); + assert!(parsed.type_args.args.is_empty()); assert_eq!(parsed.path.segments.len(), 3); } From 64ede15bb5bbc67ec3ebea67ec21588a35c7dfa5 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:44:49 +0200 Subject: [PATCH 10/15] Remove make_args --- .../src/types/attributes/default_impl/attribute.rs | 2 +- crates/macros/cgp-macro-core/src/types/attributes/prefix.rs | 2 +- .../src/types/attributes/use_provider/attribute.rs | 2 +- crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs | 2 +- crates/macros/cgp-macro-core/src/types/ident/type_arg.rs | 6 ------ crates/macros/cgp-macro-core/src/types/namespace/inherit.rs | 2 +- 6 files changed, 5 insertions(+), 11 deletions(-) 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 691c6db6..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 @@ -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/prefix.rs b/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs index 76f8bc03..4b845c76 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/prefix.rs @@ -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 e88836b1..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 @@ -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/cgp_impl/lowered.rs b/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs index 7d3a4f18..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 @@ -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/ident/type_arg.rs b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs index 42b7469a..f77be98d 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs @@ -130,12 +130,6 @@ pub struct TypeArgs { } impl TypeArgs { - /// Get a mutable reference to the underlying argument list. This matches the - /// `GenericArguments::make_args` API to ease migration. - pub fn make_args(&mut self) -> &mut Punctuated { - &mut self.args - } - pub fn is_empty(&self) -> bool { self.args.is_empty() } 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 2e46852c..b3eaf2f3 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs @@ -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(); From b28ab5bd01047d7d995cdae0470f99e036a08ddd Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 20 Jun 2026 23:46:25 +0200 Subject: [PATCH 11/15] Fix clippy --- .../cgp-macro-core/src/types/ident/type_generic_param.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 971a7950..f0f32242 100644 --- 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 @@ -28,7 +28,7 @@ pub enum TypeGenericParam { /// A type parameter, e.g. the `C` in `Bar`. Type(Ident), /// A const parameter, e.g. the `const N: usize` in `Bar`. - Const(ConstGenericParam), + Const(Box), } /// A const generic parameter at a definition site: the `const N: usize` in @@ -69,12 +69,12 @@ impl Parse for TypeGenericParam { )); } - return Ok(Self::Const(ConstGenericParam { + return Ok(Self::Const(Box::new(ConstGenericParam { const_token, ident, colon, ty, - })); + }))); } let ident: Ident = input.parse()?; From 98de70c0438378080076b72ec01f64ffd661b154 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 21 Jun 2026 00:07:39 +0200 Subject: [PATCH 12/15] AI revision --- .../delegate_component/statement/eval.rs | 4 +- .../src/types/generics/arguments.rs | 49 ------------------- .../cgp-macro-core/src/types/generics/mod.rs | 2 - .../src/types/ident/angle_bracketed.rs | 43 ++++++++++++++++ .../src/types/ident/ident_with_type_args.rs | 2 +- .../cgp-macro-core/src/types/ident/mod.rs | 2 + .../src/types/ident/path_with_type_args.rs | 2 +- .../src/types/ident/type_arg.rs | 37 +++----------- .../src/types/ident/type_generic_param.rs | 35 +++---------- 9 files changed, 65 insertions(+), 111 deletions(-) delete mode 100644 crates/macros/cgp-macro-core/src/types/generics/arguments.rs create mode 100644 crates/macros/cgp-macro-core/src/types/ident/angle_bracketed.rs 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 d38a4a71..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::{PathWithTypeArgs, TypeArg}; +use crate::types::ident::PathWithTypeArgs; pub trait EvalForEntries { fn eval_for_entries(&self, table_type: &Type) -> syn::Result>; @@ -54,7 +54,7 @@ impl EvalDelegateEntry for EvaluatedForEntry { // existing arguments. let namespace_path = &self.namespace.path; - let existing_args: Vec<&TypeArg> = self.namespace.type_args.args.iter().collect(); + let existing_args = self.namespace.type_args.args.iter(); parse_internal! { #namespace_path < #( #existing_args, )* #table_type, Delegate = #mapping_value > 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/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 117fb654..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 @@ -52,6 +52,6 @@ impl ToType for IdentWithTypeArgs { 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/mod.rs b/crates/macros/cgp-macro-core/src/types/ident/mod.rs index b2bc8341..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,9 +1,11 @@ +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::*; 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 index c3d29c9d..11372bc5 100644 --- 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 @@ -133,6 +133,6 @@ impl ToType for PathWithTypeArgs { impl From for Type { fn from(value: PathWithTypeArgs) -> Self { - parse_quote!(#value) + 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 index f77be98d..4243d267 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs @@ -1,10 +1,12 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::token::{Brace, Comma, Gt, Lt}; +use syn::token::{Brace, Comma}; use syn::{Error, Expr, ExprBlock, ExprLit, GenericArgument, 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>`. @@ -137,37 +139,14 @@ impl TypeArgs { impl Parse for TypeArgs { fn parse(input: ParseStream) -> syn::Result { - if !input.peek(Lt) { - return Ok(Self { - args: Punctuated::new(), - }); - } - - let _: Lt = input.parse()?; - - let mut args = Punctuated::new(); - - while !input.peek(Gt) { - args.push_value(input.parse()?); - - if input.peek(Gt) { - break; - } - - args.push_punct(input.parse()?); - } - - let _: Gt = input.parse()?; - - Ok(Self { args }) + Ok(Self { + args: parse_angle_bracketed(input)?, + }) } } impl ToTokens for TypeArgs { fn to_tokens(&self, tokens: &mut TokenStream) { - if !self.args.is_empty() { - let args = &self.args; - tokens.extend(quote! { < #args > }); - } + 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 index f0f32242..0c3d323d 100644 --- 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 @@ -1,10 +1,12 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::token::{Colon, Comma, Const, Gt, Lt}; +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>`. /// @@ -138,35 +140,14 @@ impl TypeGenericParams { impl Parse for TypeGenericParams { fn parse(input: ParseStream) -> syn::Result { - let mut params = Punctuated::new(); - - if !input.peek(Lt) { - return Ok(Self { params }); - } - - let _: Lt = input.parse()?; - - while !input.peek(Gt) { - params.push_value(input.parse()?); - - if input.peek(Gt) { - break; - } - - params.push_punct(input.parse()?); - } - - let _: Gt = input.parse()?; - - Ok(Self { params }) + Ok(Self { + params: parse_angle_bracketed(input)?, + }) } } impl ToTokens for TypeGenericParams { fn to_tokens(&self, tokens: &mut TokenStream) { - if !self.params.is_empty() { - let params = &self.params; - tokens.extend(quote! { < #params > }); - } + to_tokens_angle_bracketed(&self.params, tokens); } } From 1805dec99d002e739ec3766739b9389f4d83db77 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 21 Jun 2026 00:19:56 +0200 Subject: [PATCH 13/15] AI refactoring --- .../types/attributes/use_type/attribute.rs | 7 +++- .../src/types/generics/type_generics.rs | 12 ++++++ .../src/types/ident/path_with_type_args.rs | 39 +++++++++---------- .../src/types/ident/type_arg.rs | 31 +-------------- .../src/types/ident/type_generic_param.rs | 32 +++++++++++++-- 5 files changed, 64 insertions(+), 57 deletions(-) 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 424fcc75..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 @@ -35,8 +35,11 @@ impl Parse for UseTypeAttribute { let _: At = input.parse()?; // The context type is followed by a `::`-separated trait path, so it - // must parse only a single identifier head; a full path parser would - // greedily consume the trailing `::Trait::Type`. + // 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()?; 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/path_with_type_args.rs b/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs index 11372bc5..d370ef1c 100644 --- 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 @@ -1,11 +1,10 @@ use proc_macro2::TokenStream; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::{Error, Ident, Path, PathArguments, Type, parse_quote}; +use syn::{Error, Ident, Path, PathArguments, Type, parse_quote, parse2}; use crate::traits::ToType; -use crate::types::ident::{IdentWithTypeArgs, TypeArg, TypeArgs}; +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>`. @@ -15,8 +14,8 @@ use crate::types::ident::{IdentWithTypeArgs, TypeArg, TypeArgs}; /// 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`] (no associated bindings or -/// bounds). +/// 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`) @@ -34,8 +33,12 @@ impl PathWithTypeArgs { /// The identifier of the final path segment, e.g. `Foo` in /// `path::to::Foo`. pub fn ident(&self) -> &Ident { - // A parsed `syn::Path` always has at least one segment. - &self.path.segments.last().unwrap().ident + &self + .path + .segments + .last() + .expect("PathWithTypeArgs always wraps a non-empty syn::Path") + .ident } } @@ -59,9 +62,7 @@ impl Parse for PathWithTypeArgs { let last_segment = path.segments.last_mut().unwrap(); let type_args = match &last_segment.arguments { - PathArguments::None => TypeArgs { - args: Punctuated::new(), - }, + PathArguments::None => TypeArgs::default(), PathArguments::AngleBracketed(arguments) => { // Reject turbofish (`Foo::`); only the type-position form // `Foo` is accepted, matching `IdentWithTypeArgs`. @@ -72,17 +73,13 @@ impl Parse for PathWithTypeArgs { )); } - let mut args = Punctuated::new(); - - for pair in arguments.args.pairs() { - let (arg, punct) = pair.into_tuple(); - args.push_value(TypeArg::from_generic_argument(arg)?); - if let Some(comma) = punct { - args.push_punct(*comma); - } - } - - TypeArgs { args } + // 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( 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 index 4243d267..aa0a161a 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/type_arg.rs @@ -3,7 +3,7 @@ use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::{Brace, Comma}; -use syn::{Error, Expr, ExprBlock, ExprLit, GenericArgument, Lifetime, Lit, Token, Type}; +use syn::{Error, Expr, ExprBlock, ExprLit, Lifetime, Lit, Token, Type}; use crate::types::ident::{parse_angle_bracketed, to_tokens_angle_bracketed}; @@ -32,35 +32,6 @@ pub enum TypeArg { Const(Expr), } -impl TypeArg { - /// Convert a [`syn::GenericArgument`] into a [`TypeArg`], rejecting the - /// associated-binding and bound forms that are not valid in type-argument - /// positions. - /// - /// This is useful when post-processing a value that `syn` has already - /// parsed into a [`syn::Path`] (see [`PathWithTypeArgs`]). - /// - /// [`PathWithTypeArgs`]: crate::types::ident::PathWithTypeArgs - pub fn from_generic_argument(arg: &GenericArgument) -> syn::Result { - match arg { - GenericArgument::Lifetime(life) => Ok(Self::Lifetime(life.clone())), - GenericArgument::Type(ty) => Ok(Self::Type(ty.clone())), - GenericArgument::Const(expr) => Ok(Self::Const(expr.clone())), - GenericArgument::AssocType(_) | GenericArgument::AssocConst(_) => { - Err(Error::new_spanned( - arg, - "associated bindings (`Name = ...`) are not allowed in type arguments", - )) - } - GenericArgument::Constraint(_) => Err(Error::new_spanned( - arg, - "associated type bounds (`Name: ...`) are not allowed in type arguments", - )), - _ => Err(Error::new_spanned(arg, "unsupported generic argument")), - } - } -} - impl Parse for TypeArg { fn parse(input: ParseStream) -> syn::Result { if input.peek(Lifetime) { 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 index 0c3d323d..8616bc77 100644 --- 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 @@ -19,10 +19,13 @@ use crate::types::ident::{parse_angle_bracketed, to_tokens_angle_bracketed}; /// - defaults, e.g. `A = B` or `const N: usize = 0`, /// - composite forms, e.g. `(A, B)`. /// -/// The existing `TypeGenerics` type approximates this by parsing a full -/// `syn::Generics` and then round-tripping it through `split_for_impl` to -/// detect bounds. Modelling the valid forms directly is both clearer and -/// catches more invalid inputs (such as defaults) up front. +/// 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>`. @@ -117,6 +120,27 @@ impl ToTokens for TypeGenericParam { /// 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. From 5c639e06278d4bbd6b59f834b8e1818a7dd56094 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 21 Jun 2026 00:21:53 +0200 Subject: [PATCH 14/15] Move ident_with_type_params_tests --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 1 + crates/tests/cgp-macro-tests/Cargo.toml | 19 +++++++++++++++++++ crates/tests/cgp-macro-tests/src/lib.rs | 0 .../tests/ident_with_type_params.rs | 0 .../tests/ident_with_type_params_tests/mod.rs | 0 .../new_ident_with_type_args.rs | 0 .../new_ident_with_type_generics.rs | 0 .../path_with_type_args.rs | 0 9 files changed, 32 insertions(+) create mode 100644 crates/tests/cgp-macro-tests/Cargo.toml create mode 100644 crates/tests/cgp-macro-tests/src/lib.rs rename crates/tests/{cgp-tests => cgp-macro-tests}/tests/ident_with_type_params.rs (100%) rename crates/tests/{cgp-tests => cgp-macro-tests}/tests/ident_with_type_params_tests/mod.rs (100%) rename crates/tests/{cgp-tests => cgp-macro-tests}/tests/ident_with_type_params_tests/new_ident_with_type_args.rs (100%) rename crates/tests/{cgp-tests => cgp-macro-tests}/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs (100%) rename crates/tests/{cgp-tests => cgp-macro-tests}/tests/ident_with_type_params_tests/path_with_type_args.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 0b28ed56..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" 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/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..e69de29b diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params.rs similarity index 100% rename from crates/tests/cgp-tests/tests/ident_with_type_params.rs rename to crates/tests/cgp-macro-tests/tests/ident_with_type_params.rs diff --git a/crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/mod.rs similarity index 100% rename from crates/tests/cgp-tests/tests/ident_with_type_params_tests/mod.rs rename to crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/mod.rs diff --git a/crates/tests/cgp-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 similarity index 100% rename from crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs rename to crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_args.rs diff --git a/crates/tests/cgp-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 similarity index 100% rename from crates/tests/cgp-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs rename to crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/new_ident_with_type_generics.rs diff --git a/crates/tests/cgp-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 similarity index 100% rename from crates/tests/cgp-tests/tests/ident_with_type_params_tests/path_with_type_args.rs rename to crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/path_with_type_args.rs From 66a7a822f428736ec87447bf0917a721a85b9444 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 21 Jun 2026 00:30:15 +0200 Subject: [PATCH 15/15] Remove stale comments --- crates/tests/cgp-macro-tests/src/lib.rs | 1 + .../tests/ident_with_type_params_tests/mod.rs | 11 ----------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/tests/cgp-macro-tests/src/lib.rs b/crates/tests/cgp-macro-tests/src/lib.rs index e69de29b..8b137891 100644 --- a/crates/tests/cgp-macro-tests/src/lib.rs +++ 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_tests/mod.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params_tests/mod.rs index e9e5679f..0a270c04 100644 --- 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 @@ -1,14 +1,3 @@ -//! Tests exploring the parsing behavior of the ident/path-with-type-parameter -//! constructs in `cgp-macro-core`. -//! -//! These cover both the original constructs (`IdentWithTypeArgs`, -//! `IdentWithTypeGenerics`) and the new ones (`IdentWithTypeArgs`, -//! `IdentWithTypeGenerics`, `PathWithTypeArgs`), including a side-by-side -//! comparison that documents exactly where their behaviors diverge. -//! -//! See `crates/macros/cgp-macro-core/src/types/ident/README.md` for the prose -//! explanation that accompanies these tests. - pub mod new_ident_with_type_args; pub mod new_ident_with_type_generics; pub mod path_with_type_args;