diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index cb6df780c..b70c55971 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -25,12 +25,13 @@ pluralizer = { version = "0.5" } proc-macro-crate = { version = "3.2.0", optional = true } proc-macro2 = { version = "1", default-features = false } quote = { version = "1", default-features = false } -syn = { version = "2", default-features = false, features = [ +syn = { version = "3", default-features = false, features = [ "parsing", "proc-macro", "derive", "printing", "extra-traits", + "full", ] } unicode-ident = { version = "1" } diff --git a/sea-orm-macros/src/derives/active_model.rs b/sea-orm-macros/src/derives/active_model.rs index c1c53ac5a..868d28834 100644 --- a/sea-orm-macros/src/derives/active_model.rs +++ b/sea-orm-macros/src/derives/active_model.rs @@ -4,16 +4,16 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use syn::{Data, DataStruct, Expr, Fields, LitStr, Type, Visibility}; -pub(crate) struct DeriveActiveModel { +pub(crate) struct DeriveActiveModel<'a> { model: Ident, - vis: Visibility, + vis: &'a Visibility, fields: Vec, names: Vec, - types: Vec, + types: Vec<&'a Type>, } -impl DeriveActiveModel { - pub fn new(vis: &Visibility, ident: &Ident, data: &Data) -> syn::Result { +impl<'a> DeriveActiveModel<'a> { + pub fn new(vis: &'a Visibility, ident: &Ident, data: &'a Data) -> syn::Result { let all_fields = match data { Data::Struct(DataStruct { fields: Fields::Named(named), @@ -60,12 +60,12 @@ impl DeriveActiveModel { })?; names.push(ident); - types.push(field.ty.clone()); + types.push(&field.ty); } Ok(DeriveActiveModel { model: ident.clone(), - vis: vis.clone(), + vis, fields, names, types, @@ -73,9 +73,9 @@ impl DeriveActiveModel { } } -impl DeriveActiveModel { +impl<'a> DeriveActiveModel<'a> { fn define_active_model(&self) -> TokenStream { - let vis = &self.vis; + let vis = self.vis; let fields = &self.fields; let types = &self.types; quote!( diff --git a/sea-orm-macros/src/derives/active_model_ex.rs b/sea-orm-macros/src/derives/active_model_ex.rs index b095a2836..ab0a112af 100644 --- a/sea-orm-macros/src/derives/active_model_ex.rs +++ b/sea-orm-macros/src/derives/active_model_ex.rs @@ -8,7 +8,7 @@ use super::util::{ use heck::ToUpperCamelCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; -use syn::{Attribute, Data, LitStr, PathArguments, Type, TypePath, Visibility}; +use syn::{Attribute, Data, LitStr, PathArguments, Type, TypePath, Visibility, parse_quote}; enum RelationAttr { BelongsTo { @@ -79,8 +79,8 @@ impl RelationAttr { )); } Ok(Some(Self::BelongsTo { - from: RelationColumns::from_lit(from.clone())?, - relation_enum: Some(relation_enum.clone()), + from: RelationColumns::from_lit(from)?, + relation_enum: Some(parse_quote!(#relation_enum)), })) } compound_attr::SeaOrm { @@ -101,7 +101,7 @@ impl RelationAttr { )); } Ok(Some(Self::HasManySelf { - relation_enum: relation_enum.clone(), + relation_enum: parse_quote!(#relation_enum), })) } compound_attr::SeaOrm { @@ -130,10 +130,10 @@ impl RelationAttr { )); } Ok(Some(Self::BelongsTo { - from: RelationColumns::from_lit(attrs.from.clone().ok_or_else(|| { + from: RelationColumns::from_lit(attrs.from.as_ref().ok_or_else(|| { syn::Error::new_spanned(field_ident, "belongs_to must specify `from`") })?)?, - relation_enum: attrs.relation_enum.clone(), + relation_enum: attrs.relation_enum.as_ref().map(|lit| parse_quote!(#lit)), })) } compound_attr::SeaOrm { @@ -446,7 +446,8 @@ impl<'a> ActiveModelSetter<'a> { fn expand_compound(&self, compound_type: &CompoundType) -> syn::Result { let field_ident = self.field.ident; let entity_path = &compound_type.entity; - let mut active_model_type = entity_path.path.clone(); + let entity_path_path = &entity_path.path; + let mut active_model_type: syn::Path = parse_quote!(#entity_path_path); let Some(segment) = active_model_type.segments.last_mut() else { return Err(syn::Error::new_spanned(entity_path, "expected entity path")); }; @@ -600,7 +601,7 @@ impl ActiveModelActionTokens { RelationAttr::HasManySelf { relation_enum } => { Some(Relation::HasManySelf(HasManySelfField { ident, - relation_variant: relation_enum.clone(), + relation_variant: parse_quote!(#relation_enum), })) } RelationAttr::BelongsTo { diff --git a/sea-orm-macros/src/derives/arrow_schema.rs b/sea-orm-macros/src/derives/arrow_schema.rs index 346096fd5..3cab0703f 100644 --- a/sea-orm-macros/src/derives/arrow_schema.rs +++ b/sea-orm-macros/src/derives/arrow_schema.rs @@ -92,7 +92,7 @@ pub fn expand_derive_arrow_schema( fields_info.push(ArrowFieldInfo { name: resolved_name, - field_type: field_type.clone(), + field_type: syn::parse_quote!(#field_type), column_type_str, nullable, arrow_attrs, diff --git a/sea-orm-macros/src/derives/entity_loader.rs b/sea-orm-macros/src/derives/entity_loader.rs index a9bc84592..fd91eb00b 100644 --- a/sea-orm-macros/src/derives/entity_loader.rs +++ b/sea-orm-macros/src/derives/entity_loader.rs @@ -47,6 +47,11 @@ struct EntityLoaderOutput { } impl EntityLoaderField { + fn entity_path(&self) -> syn::Path { + let path = &self.entity.path; + syn::parse_quote!(#path) + } + fn expand_loader_with_field_into(&self, output: &mut EntityLoaderOutput) { let field = &self.field; @@ -62,7 +67,7 @@ impl EntityLoaderField { EntityLoaderFieldKind::HasOne | EntityLoaderFieldKind::HasMany | EntityLoaderFieldKind::ManyToMany => { - let mut entity_module = self.entity.path.clone(); + let mut entity_module = self.entity_path(); entity_module.segments.pop(); entity_module .segments @@ -81,9 +86,10 @@ impl EntityLoaderField { } fn expand_relation_tuple_with_param_into(&self, output: &mut EntityLoaderOutput) { - let mut entity_module = self.entity.path.clone(); + let mut entity_module = self.entity_path(); entity_module.segments.pop(); - let mut related_entity = entity_module.clone(); + // syn 3's `pop()` leaves a trailing `::`, which `push` reuses as the separator. + let mut related_entity: syn::Path = entity_module.clone(); related_entity.segments.push(syn::parse_quote!(Entity)); let mut related_relation = entity_module; related_relation.segments.push(syn::parse_quote!(Relation)); @@ -294,7 +300,7 @@ impl EntityLoaderField { } else { quote!() }; - let mut entity_module = self.entity.path.clone(); + let mut entity_module = self.entity_path(); entity_module.segments.pop(); entity_module.segments.push(syn::parse_quote!(EntityLoader)); @@ -345,7 +351,7 @@ impl EntityLoaderField { } else { quote!() }; - let mut entity_module = self.entity.path.clone(); + let mut entity_module = self.entity_path(); entity_module.segments.pop(); entity_module.segments.push(syn::parse_quote!(EntityLoader)); @@ -403,7 +409,7 @@ impl EntityLoaderField { } else { quote!() }; - let mut entity_module = self.entity.path.clone(); + let mut entity_module = self.entity_path(); entity_module.segments.pop(); entity_module.segments.push(syn::parse_quote!(EntityLoader)); @@ -454,7 +460,7 @@ impl EntityLoaderField { } else { quote!() }; - let mut entity_module = self.entity.path.clone(); + let mut entity_module = self.entity_path(); entity_module.segments.pop(); entity_module.segments.push(syn::parse_quote!(EntityLoader)); @@ -994,3 +1000,40 @@ pub fn expand_entity_loader(vis: &Visibility, schema: EntityLoaderSchema) -> Tok } } + +#[cfg(test)] +mod test { + use super::*; + use proc_macro2::Span; + use syn::parse_quote; + + #[test] + fn expand_relation_tuple_multi_segment_entity_path() { + // Verifies the multi-segment entity module path is extended correctly + // (syn 3 `pop()` leaves a trailing `::`, reused as the separator). + let field = EntityLoaderField { + field: syn::Ident::new("cakes", Span::call_site()), + entity: parse_quote!(crate::entities::cake::Entity), + relation_enum: None, + kind: EntityLoaderFieldKind::HasMany, + }; + + let mut output = EntityLoaderOutput::default(); + field.expand_relation_tuple_with_param_into(&mut output); + + let generated = output.with_param_impls.to_string(); + assert!( + generated.contains("crate :: entities :: cake :: Entity"), + "expected related entity path, got: {generated}" + ); + assert!( + generated.contains("crate :: entities :: cake :: Relation"), + "expected related relation path, got: {generated}" + ); + // No dangling or doubled `::` in the emitted paths. + assert!( + !generated.contains(":: ::"), + "unexpected double colon in generated path: {generated}" + ); + } +} diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index 8a877fa61..acacd1d99 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -302,7 +302,7 @@ pub fn expand_derive_entity_model( ignore = true; } else if meta.path.is_ident("primary_key") { is_primary_key = true; - primary_key_types.push(field.ty.clone()); + primary_key_types.push(&field.ty); } else if meta.path.is_ident("nullable") { nullable = true; } else if meta.path.is_ident("indexed") { diff --git a/sea-orm-macros/src/derives/into_active_model.rs b/sea-orm-macros/src/derives/into_active_model.rs index 4a0a6f2c0..d40371adf 100644 --- a/sea-orm-macros/src/derives/into_active_model.rs +++ b/sea-orm-macros/src/derives/into_active_model.rs @@ -140,7 +140,8 @@ impl DeriveIntoActiveModel { } = self; let mut active_model_ident = active_model - .clone() + .as_ref() + .map(|am| syn::parse_quote!(#am)) .unwrap_or_else(|| syn::parse_str::("ActiveModel").unwrap()); // Create a type alias for qualified types @@ -148,6 +149,7 @@ impl DeriveIntoActiveModel { let type_alias = format_ident!("ActiveModelFor{ident}"); let type_def = quote!( type #type_alias = #active_model_ident; ); active_model_ident = syn::Type::Path(syn::TypePath { + attrs: Vec::new(), qself: None, path: syn::Path { leading_colon: None, @@ -180,7 +182,10 @@ impl DeriveIntoActiveModel { }); // Add custom field assignments from #[sea_orm(set(field = expr))] - let (set_idents, set_exprs): (Vec<_>, Vec<_>) = set_fields.iter().cloned().unzip(); + let (set_idents, set_exprs): (Vec<_>, Vec<_>) = set_fields + .iter() + .map(|(id, expr)| (id.clone(), expr)) + .unzip(); let expanded_sets = set_exprs.iter().map(|expr| { quote!( sea_orm::ActiveValue::Set(#expr) diff --git a/sea-orm-macros/src/derives/model.rs b/sea-orm-macros/src/derives/model.rs index d29a69b06..63e44a984 100644 --- a/sea-orm-macros/src/derives/model.rs +++ b/sea-orm-macros/src/derives/model.rs @@ -9,17 +9,17 @@ use quote::{format_ident, quote}; use std::iter::FromIterator; use syn::{Attribute, Data, Expr, Ident, LitStr, Type}; -pub(crate) struct DeriveModel { +pub(crate) struct DeriveModel<'a> { column_idents: Vec, entity_ident: Ident, field_idents: Vec, - field_types: Vec, + field_types: Vec<&'a syn::Type>, ident: Ident, ignore_attrs: Vec, } -impl DeriveModel { - pub fn new(ident: &Ident, data: &Data, attrs: &[Attribute]) -> syn::Result { +impl<'a> DeriveModel<'a> { + pub fn new(ident: &Ident, data: &'a Data, attrs: &[Attribute]) -> syn::Result { let fields = match data { syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(syn::FieldsNamed { named, .. }), @@ -42,7 +42,7 @@ impl DeriveModel { .map(|field| field.ident.as_ref().unwrap().clone()) .collect(); - let field_types = fields.iter().map(|field| field.ty.clone()).collect(); + let field_types = fields.iter().map(|field| &field.ty).collect(); let column_idents = fields .iter() @@ -188,11 +188,11 @@ impl DeriveModel { ) } - pub fn impl_model_trait<'a>(&'a self) -> TokenStream { + pub fn impl_model_trait<'b>(&'b self) -> TokenStream { let ident = &self.ident; let entity_ident = &self.entity_ident; let ignore_attrs = &self.ignore_attrs; - let ignore = |(ident, ignore): (&'a Ident, &bool)| -> Option<&'a Ident> { + let ignore = |(ident, ignore): (&'b Ident, &bool)| -> Option<&'b Ident> { if *ignore { None } else { Some(ident) } }; let field_idents: Vec<&Ident> = self diff --git a/sea-orm-macros/src/derives/model_ex.rs b/sea-orm-macros/src/derives/model_ex.rs index b1dc78d54..4301e0936 100644 --- a/sea-orm-macros/src/derives/model_ex.rs +++ b/sea-orm-macros/src/derives/model_ex.rs @@ -28,12 +28,25 @@ pub fn expand_sea_orm_model(input: ItemStruct, compact: bool) -> syn::Result 1 { + continue; + } + let mut other_attrs = Punctuated::::new(); attr.parse_nested_meta(|meta| { @@ -74,7 +87,7 @@ pub fn expand_sea_orm_model(input: ItemStruct, compact: bool) -> syn::Result EntityLoaderF let self_entity = is_self_entity(entity); let (relation_enum, kind) = match &compound.relation { Some(RelationAttr::BelongsTo(attr)) => ( - attr.relation_variants.explicit_name().cloned(), + attr.relation_variants + .explicit_name() + .map(|lit| parse_quote!(#lit)), if self_entity { EntityLoaderFieldKind::HasOneSelf } else { @@ -481,7 +496,9 @@ fn entity_loader_field(field: &Ident, compound: &CompoundField) -> EntityLoaderF }, ), Some(RelationAttr::HasOne(attr)) => ( - attr.relation_variants.explicit_name().cloned(), + attr.relation_variants + .explicit_name() + .map(|lit| parse_quote!(#lit)), if self_entity { EntityLoaderFieldKind::HasOneSelf } else { @@ -496,7 +513,9 @@ fn entity_loader_field(field: &Ident, compound: &CompoundField) -> EntityLoaderF reverse, .. } => ( - relation_variants.explicit_name().cloned(), + relation_variants + .explicit_name() + .map(|lit| parse_quote!(#lit)), via.as_ref().map(|junction| junction.module.clone()), *reverse, ), @@ -505,7 +524,7 @@ fn entity_loader_field(field: &Ident, compound: &CompoundField) -> EntityLoaderF junction_module, direction, } => ( - relation_enum.clone(), + parse_quote!(#relation_enum), Some(junction_module.clone()), matches!(direction, ManyToManySelfDirection::Reverse), ), @@ -540,7 +559,7 @@ fn entity_loader_field(field: &Ident, compound: &CompoundField) -> EntityLoaderF }; EntityLoaderField { field: field.clone(), - entity: entity.clone(), + entity: parse_quote!(#entity), relation_enum, kind, } @@ -748,14 +767,14 @@ impl BelongsToAttr { to }; - let from = match from.map(RelationColumns::from_lit).transpose() { + let from = match from.map(|lit| RelationColumns::from_lit(&lit)).transpose() { Ok(from) => from, Err(err) => { combine_error(&mut error, err); None } }; - let to = match to.map(RelationColumns::from_lit).transpose() { + let to = match to.map(|lit| RelationColumns::from_lit(&lit)).transpose() { Ok(to) => to, Err(err) => { combine_error(&mut error, err); @@ -1456,17 +1475,18 @@ fn expand_find_by_unique_key(schema: &ModelExSchema<'_>) -> (TokenStream, TokenS let ModelExFieldKind::Scalar(scalar) = &field.kind else { continue; }; + let scalar_ty = &scalar.ty; if scalar.unique { unique_keys.insert( field.ident.clone(), - vec![(field.ident.clone(), scalar.ty.clone())], + vec![(field.ident.clone(), parse_quote!(#scalar_ty))], ); } for unique_key in &scalar.unique_keys { unique_keys .entry(unique_key.clone()) .or_default() - .push((field.ident.clone(), scalar.ty.clone())); + .push((field.ident.clone(), parse_quote!(#scalar_ty))); } } diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index c72882de4..fc4699b77 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -176,7 +176,8 @@ impl DerivePartialModel { }, (None, None, true) => { if let Some(prefix) = &nested_prefix { - let key = (field.ty.clone(), Some(prefix.clone())); + let field_ty = &field.ty; + let key = (syn::parse_quote!(#field_ty), Some(prefix.clone())); match seen_nested.entry(key) { Entry::Occupied(e) => { let msg = format!( @@ -265,7 +266,7 @@ impl DerivePartialModel { let impl_into_active_model = if self.into_active_model { DeriveIntoActiveModel { ident: self.ident.clone(), - active_model: self.active_model.clone(), + active_model: self.active_model.as_ref().map(|t| syn::parse_quote!(#t)), fields: self .fields .iter() diff --git a/sea-orm-macros/src/derives/util.rs b/sea-orm-macros/src/derives/util.rs index 19a72c535..c405eda7d 100644 --- a/sea-orm-macros/src/derives/util.rs +++ b/sea-orm-macros/src/derives/util.rs @@ -3,7 +3,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::{ Field, GenericArgument, LitStr, Meta, MetaNameValue, PathArguments, Type, TypePath, - meta::ParseNestedMeta, punctuated::Punctuated, token::Comma, + meta::ParseNestedMeta, parse_quote, punctuated::Punctuated, token::Comma, }; pub(crate) fn async_token() -> TokenStream { @@ -32,7 +32,7 @@ impl RelationColumns { /// For example: /// `cake_id` or `Column::CakeId` -> `CakeId`; /// `(user_id, post_id)` -> `UserId`, `PostId`. - pub(crate) fn from_lit(lit: LitStr) -> syn::Result { + pub(crate) fn from_lit(lit: &LitStr) -> syn::Result { let paths = if lit.value().starts_with('(') { lit.parse_with(|input: syn::parse::ParseStream<'_>| { let content; @@ -114,7 +114,6 @@ pub(crate) enum CompoundKind { HasMany, } -#[derive(Clone)] pub(crate) struct CompoundType { pub(crate) kind: CompoundKind, pub(crate) entity: TypePath, @@ -169,7 +168,7 @@ impl CompoundType { ) { ("Entity", _) => Ok(Some(Self { kind: CompoundKind::BelongsTo(CardinalityKind::Required), - entity: ty_path.clone(), + entity: parse_quote!(#ty_path), })), ("Option", PathArguments::AngleBracketed(args)) => { let Some(entity) = entity_generic_arg(&args.args) else { @@ -285,7 +284,7 @@ fn entity_generic_arg(args: &Punctuated) -> Option { - Some(type_path.clone()) + Some(parse_quote!(#type_path)) } _ => None, }