/* * Copyright 2024 Luca Fulchir * * Licensed under the Apache License, Version 2.0 with LLVM exception (the * "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License and of the exception at * * http://www.apache.org/licenses/LICENSE-2.0 * https://spdx.org/licenses/LLVM-exception.html * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use ::proc_macro::TokenStream; use ::quote::quote; use ::syn::{parse_macro_input, DeriveInput, Fields, ItemImpl, ItemStruct}; pub(crate) fn repository( attrs: TokenStream, input: TokenStream, ) -> TokenStream { let local = parse_macro_input!(input as ItemStruct); let Fields::Named(local_fields) = local.fields else { use ::syn::spanned::Spanned; return ::syn::Error::new( local.fields.span(), "unnamed fields are not supported", ) .to_compile_error() .into(); }; // do not duplicate token export or derive macro let local_attrs = local.attrs.iter().filter(|&x| { match &x.meta { ::syn::Meta::Path(p) => { // looking for: // #[::macro_magic::export_tokens] if p.segments.len() == 2 && p.segments[0].ident == "macro_magic" && p.segments[1].ident == "export_tokens" { false } else { true } } ::syn::Meta::List(ml) => { // looking for: // #[derive(::bok::Repository, Debug)] if ml.path.segments.len() > 0 && ml.path.is_ident("derive") { use ::syn::{punctuated::Punctuated, *}; if let Ok(v) = ml.parse_args_with( Punctuated::<::syn::Path, Token![,]>::parse_terminated, ) { if v.len() == 2 && v[0].segments.len() == 2 && v[0].segments[0].ident.to_string() == "bok" && v[0].segments[1].ident.to_string() == "Repository" && v[1].segments.len() == 1 && v[1].segments[0].ident.to_string() == "Debug" { return false; } } } true } _ => true, } }); let (_, generics, where_clause) = local.generics.split_for_impl(); let ident = local.ident; let vis = local.vis; let base = parse_macro_input!(attrs as ItemStruct); let Fields::Named(base_fields) = &base.fields else { use ::syn::spanned::Spanned; return ::syn::Error::new( base.fields.span(), "`#[::bok::repository(..)]`: base has unsupported unnamed fields", ) .to_compile_error() .into(); }; // make sure base is a repo if base_fields .named .iter() .find(|&x| { x.ident .as_ref() .is_some_and(|id| id.to_string() == "_bok_repo") }) .is_none() { use ::syn::spanned::Spanned; return ::syn::Error::new( base.fields.span(), "`#[::bok::repository(..)]` base is not a repo", ) .to_compile_error() .into(); } let mut all_fields = Vec::<::syn::Field>::with_capacity( local_fields.named.len() + base_fields.named.len(), ); all_fields.extend(local_fields.named.iter().cloned()); // make sure there is always `_bok_repo` marker if local_fields .named .iter() .find(|&f| { f.ident .as_ref() .is_some_and(|id| id.to_string() == "_bok_repo") }) .is_none() { all_fields.push(::syn::parse_quote! { _bok_repo: ::std::marker::PhantomData<::bok::RepositoryEmpty> }); } for b_f in base_fields.named.iter() { let Some(b_f_id) = &b_f.ident else { continue }; let b_f_id_str = b_f_id.to_string(); if all_fields .iter() .find(|&f| { f.ident .as_ref() .is_some_and(|id| id.to_string() == b_f_id_str) }) .is_none() { all_fields.push(b_f.clone()); } } quote! { #(#local_attrs) * #[::macro_magic::export_tokens] #[derive(::bok::Repository, Debug)] #vis struct #ident #generics #where_clause { #(#all_fields), * } } .into() } pub(crate) fn derive_repository(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = input.ident.clone(); let ::syn::Data::Struct(items) = &input.data else { use ::syn::spanned::Spanned; return ::syn::Error::new( input.span(), "#[derive(::bok::Repository)]: not called on a struct", ) .to_compile_error() .into(); }; if items .fields .iter() .find(|&f| { f.ident .as_ref() .is_some_and(|id| id.to_string() == "_bok_repo") }) .is_none() { use ::syn::spanned::Spanned; return ::syn::Error::new( input.span(), "#[derive(::bok::Repository)]: struct is not a bok repo. use \ `#[::bok::repository(..)]` first", ) .to_compile_error() .into(); } // holds the list of all package names, snake case let mut all_pkgs = Vec::<::syn::Ident>::with_capacity(items.fields.len()); for it in items.fields.iter() { let Some(id) = &it.ident else { continue }; let id_str = id.to_string(); if id_str.starts_with("_p_") { let name = id_str.strip_prefix("_p_").unwrap().to_owned(); all_pkgs.push(::quote::format_ident!("{}", name)); } } all_pkgs.sort_by(|a, b| a.to_string().cmp(&b.to_string())); let pkgs_num: usize = all_pkgs.len(); quote! { impl ::bok::Repository for #name { fn name(&self) -> ::bok::RepoName { (module_path!().to_owned() + "::" + ::std::stringify!(#name)).as_str().into() } fn path(&self) -> ::bok::Path<::bok::RepoName> { (module_path!().to_owned() + "::" + ::std::stringify!(#name)).as_str().into() } fn pkg_list(&self) -> &[&'static str] { const PKGS : [&'static str; #pkgs_num] = [ #(stringify!(#all_pkgs)), * ]; &PKGS } fn get(&self, pkg_name: &str) -> Option<::std::boxed::Box> { match pkg_name { #(#all_pkgs => Some(::std::boxed::Box::new(self.#all_pkgs())),) * _ => None, } } } }.into() } pub(crate) fn repo_packages( attrs: TokenStream, input: TokenStream, ) -> TokenStream { let local = parse_macro_input!(input as ItemStruct); let mut packages = parse_macro_input!(attrs as crate::PathList); // make sure all the packages have generics, and if not add the repo as // generic aka: rewrite // "#[repo_packages(my::pkg)]" -->> "#[repo_packages(my::pkg)])" let mut rewrite = false; let repo_argument = { let generic_id: ::syn::Path = ::syn::parse_quote!(id); generic_id.segments.last().unwrap().arguments.clone() }; for p in packages.0.iter_mut() { let last = p.segments.last_mut().unwrap(); if last.arguments.is_empty() { rewrite = true; last.arguments = repo_argument.clone(); } } if rewrite { let p_list = packages.0.into_iter(); return quote! { #[::bok::repo_packages(#(#p_list,)*)] #local } .into(); } let packages = packages; // remove mut let local_attrs = local.attrs.iter(); let (_, generics, where_clause) = local.generics.split_for_impl(); let ident = local.ident; let vis = local.vis; use ::syn::spanned::Spanned; let Fields::Named(ref local_fields) = local.fields else { use ::syn::spanned::Spanned; return ::syn::Error::new( local.fields.span(), "#[repo_packages(..)]: unnamed fields are not supported", ) .to_compile_error() .into(); }; // find the marker. we need it to separate things added manually // from things we get by extending the other repositories let Some(marker) = local_fields.named.iter().find(|&f| { f.ident .as_ref() .is_some_and(|id| id.to_string() == "_bok_repo") }) else { use ::syn::spanned::Spanned; return ::syn::Error::new( local_fields.span(), "#[repo_packages(..)]: struct is not a repository. Forgot \ '#[::bok::repository(..)]` first?", ) .to_compile_error() .into(); }; let fields_up_to_marker = local_fields .named .iter() .take_while(|&f| { f.ident .as_ref() .is_some_and(|id| id.to_string() != "_bok_repo") }) .collect::>(); let mut fields_after_marker = local_fields .named .iter() .skip_while(|&f| { f.ident .as_ref() .is_some_and(|id| id.to_string() != "_bok_repo") }) .skip_while(|&f| { f.ident .as_ref() .is_some_and(|id| id.to_string() == "_bok_repo") }) .collect::>(); // the packages added manually must not be repeated manually. // but they will override any other package added by // extending the repository let mut fields_added = Vec::<::syn::Field>::with_capacity(packages.0.len()); for p in packages.0 { let path_ident = &p.segments.last().unwrap().ident; use ::convert_case::{Case, Casing}; let pkg_id = "_p_".to_owned() + path_ident.to_string().to_case(Case::Snake).as_str(); if fields_up_to_marker .iter() .find(|&f| { if let Some(id) = &f.ident { id.to_string() == pkg_id } else { false } }) .is_some() { use ::syn::spanned::Spanned; return ::syn::Error::new( local_fields.span(), "#[repo_packages(..)]: package already present: ".to_owned() + pkg_id.as_str(), ) .to_compile_error() .into(); } if fields_added .iter() .find(|&f| { if let Some(id) = &f.ident { id.to_string() == pkg_id } else { false } }) .is_some() { use ::syn::spanned::Spanned; return ::syn::Error::new( local_fields.span(), "#[repo_packages(..)]: package added twice: ".to_owned() + pkg_id.as_str(), ) .to_compile_error() .into(); } let pkg_ident = ::quote::format_ident!("{}", pkg_id, span = local_fields.span()); let new_pkg: ::syn::Field = ::syn::parse_quote! { #pkg_ident : ::std::marker::PhantomData<#p> }; fields_added.push(new_pkg); fields_after_marker.retain(|&f| { f.ident.as_ref().is_some_and(|id| id.to_string() != pkg_id) }); } let mut all_fields = Vec::with_capacity(local_fields.named.len() + fields_added.len()); all_fields.extend(fields_up_to_marker.iter()); all_fields.extend(fields_added.iter()); all_fields.push(marker); all_fields.extend(fields_after_marker.iter()); quote! { #(#local_attrs) * #vis struct #ident #generics #where_clause { #(#all_fields), * } } .into() } pub(crate) fn repo_impl( _attrs: TokenStream, input: TokenStream, ) -> TokenStream { let local = parse_macro_input!(input as ItemImpl); let reponame = &local.self_ty; quote! { #[::bok_macro::repo_impl_methods(#reponame)] #local } .into() } pub(crate) fn repo_impl_methods( attrs: TokenStream, input: TokenStream, __source_name: TokenStream, ) -> TokenStream { let base = parse_macro_input!(attrs as ItemStruct); let local = parse_macro_input!(input as ItemImpl); let source_name = parse_macro_input!(__source_name as ::syn::Path); if let ::syn::Type::Path(self_type_path) = local.self_ty.as_ref() { if self_type_path.path != source_name { return ::syn::Error::new( proc_macro2::Span::call_site(), "#[::bok_macro::repo_impl_methods(..)]: argument and impl \ type differ", ) .to_compile_error() .into(); } } else { return ::syn::Error::new( proc_macro2::Span::call_site(), "#[::bok_macro::repo_impl_methods(..)]: argument and impl type \ differ", ) .to_compile_error() .into(); } let local_attrs = local.attrs.iter(); let (_, generics, where_clause) = local.generics.split_for_impl(); let ::syn::Type::Path(local_tp) = local.self_ty.as_ref() else { return ::syn::Error::new( proc_macro2::Span::call_site(), "#[::bok_macro::repo_impl_methods(..)]: no ident?", ) .to_compile_error() .into(); }; let local_ident = local_tp.path.get_ident().expect("NOT AN IDENT"); let items = local.items.iter(); // FIXME: make sure `items` does not have methods that are not packages in // the impl let ::syn::Fields::Named(base_fields) = base.fields else { return ::syn::Error::new( proc_macro2::Span::call_site(), "#[::bok_macro::repo_impl_methods(..)]: type has unsupported \ unnamed fields", ) .to_compile_error() .into(); }; let mut all_pkgs_types = Vec::with_capacity(base_fields.named.len()); let mut fn_to_add = Vec::new(); for f in base_fields.named.iter() { if !f .ident .as_ref() .is_some_and(|id| id.to_string().starts_with("_p_")) { continue; }; let ::syn::Type::Path(f_type) = &f.ty else { continue; }; let t_phantom = &f_type.path; let t_id = { let args = &t_phantom .segments .last() .expect("t_phantom no last?") .arguments; let ::syn::PathArguments::AngleBracketed(bracketed) = &args else { panic!("phantom without anglebracket?"); }; let ::syn::GenericArgument::Type(::syn::Type::Path(t)) = bracketed.args.first().expect("phantom bracketed, no args") else { panic!("phantom bracketed, generic not a type path"); }; t.path.clone() }; let pkg_name = { let segment = t_id.segments.iter().last().unwrap(); use ::convert_case::{Case, Casing}; ::quote::format_ident!( "{}", segment.ident.to_string().to_case(Case::Snake) ) }; all_pkgs_types.push(t_id.clone()); if local .items .iter() .find(|&func_it| { let ::syn::ImplItem::Fn(func) = &func_it else { return false; }; func.sig.ident.to_string() == pkg_name.to_string() }) .is_some() { // the user overrode the `::default()` package build // don't try to add it again continue; } let mut t_id_build = t_id.clone(); if let ::syn::PathArguments::AngleBracketed(args) = &mut t_id_build.segments.last_mut().unwrap().arguments { args.colon2_token = Some(::syn::token::PathSep::default()); } let pkg_fn: ::syn::ImplItemFn = ::syn::parse_quote! { pub fn #pkg_name(&self) -> #t_id { #t_id_build::default() } }; fn_to_add.push(pkg_fn); } // keep sorted for easier debugging when expanding macros fn_to_add .sort_by(|a, b| a.sig.ident.to_string().cmp(&b.sig.ident.to_string())); all_pkgs_types.sort_by(|a, b| { a.segments .last() .unwrap() .ident .to_string() .cmp(&b.segments.last().unwrap().ident.to_string()) }); let new_fn = fn_to_add.iter(); let mut all_impl_deps = Vec::with_capacity(all_pkgs_types.len()); for p_type in all_pkgs_types.into_iter() { let bok_dep_trait = { let mut tmp = p_type.clone(); let last = tmp.segments.last_mut().unwrap(); last.ident = quote::format_ident!("BokDeps{}", last.ident); last.arguments = ::syn::PathArguments::None; tmp }; let pkg_trait_impl = &bok_dep_trait; let dep_view = quote! { #[::bok_macro::repo_impl_pkg_deps(#pkg_trait_impl)] impl #bok_dep_trait for #local_ident {} }; all_impl_deps.push(dep_view); } quote! { #(#local_attrs) * impl #local_ident #generics #where_clause { #(#items) * #(#new_fn) * } #(#all_impl_deps) * } .into() } pub(crate) fn repo_impl_pkg_deps( attrs: TokenStream, input: TokenStream, ) -> TokenStream { let trait_deps = parse_macro_input!(attrs as ::syn::ItemTrait); let local = parse_macro_input!(input as ItemImpl); let impl_trait = local .trait_ .expect("#[::bok_macro::repo_impl_pkg_deps()]: no trait found") .1; let local_attrs = local.attrs.iter(); let (_, generics, _) = local.generics.split_for_impl(); let ident = &local.self_ty; //let deps = Vec::<::syn::TraitItemFn>::new(); let deps = trait_deps .items .iter() .filter_map(|x| { if let ::syn::TraitItem::Fn(func) = &x { let name = &func.sig.ident; let dep_impl: ::syn::TraitItemFn = ::syn::parse_quote! { fn #name(&self) -> ::std::boxed::Box { ::std::boxed::Box::new(self.#name()) } }; Some(dep_impl) } else { None } }) .collect::>(); quote! { #(#local_attrs) * impl #impl_trait for #ident #generics { #(#deps) * } } .into() }