diff --git a/Cargo.lock b/Cargo.lock index a90c13b..da3f1c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,6 +389,15 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "stageleft" version = "0.11.0" @@ -397,6 +406,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", + "slotmap", "stageleft_macro", "syn", "trybuild", @@ -441,6 +451,15 @@ dependencies = [ "stageleft_tool", ] +[[package]] +name = "stageleft_test_no_entry" +version = "0.0.0" +dependencies = [ + "slotmap", + "stageleft", + "stageleft_tool", +] + [[package]] name = "stageleft_tool" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 62552d5..b181652 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "stageleft_test_macro", "stageleft_test_downstream", "stageleft_tool", + "stageleft_test_no_entry", ] resolver = "2" diff --git a/stageleft/Cargo.toml b/stageleft/Cargo.toml index 80779e3..87a162e 100644 --- a/stageleft/Cargo.toml +++ b/stageleft/Cargo.toml @@ -20,4 +20,5 @@ stageleft_macro = { path = "../stageleft_macro", version = "^0.11.0" } ctor = "0.4.1" [dev-dependencies] +slotmap = "1.0.0" trybuild = "1" diff --git a/stageleft/src/lib.rs b/stageleft/src/lib.rs index 6b80c0c..1c814c7 100644 --- a/stageleft/src/lib.rs +++ b/stageleft/src/lib.rs @@ -107,6 +107,38 @@ macro_rules! stageleft_no_entry_crate { }; } +/// Re-exports items that have been generated by macros via `pub use`. +/// +/// Must be named `stageleft_export!` to work properly. (Do NOT do `use stageleft::stageleft_export as other_name;`). +/// +/// This ensures that macro-generated items that stageleft cannot handle on its own will properly use the items. +/// +/// ```rust,ignore +/// stageleft::stageleft_export!(MyKey, MyOtherKey); +/// ``` +#[macro_export] +macro_rules! stageleft_export { + ( + $( + $name:ident + ),* $(,)? + ) => { + $( + #[expect(unused_imports, reason = "ensures item exists and is pub")] + pub use self::$name as _; + )* + }; + ( + mod = $path:path + $( + , + $name:ident + )* $(,)? + ) => { + pub use $path :: { $( $name ),* }; + }; +} + pub trait QuotedContext { fn create() -> Self; } diff --git a/stageleft/tests/compile-fail/export_not_pub.rs b/stageleft/tests/compile-fail/export_not_pub.rs new file mode 100644 index 0000000..a0b2178 --- /dev/null +++ b/stageleft/tests/compile-fail/export_not_pub.rs @@ -0,0 +1,9 @@ +stageleft::stageleft_export!(PubKey, NotPubKey); + +#[cfg(stageleft_runtime)] +slotmap::new_key_type! { + pub struct PubKey; + pub(crate) struct NotPubKey; +} + +fn main() {} diff --git a/stageleft/tests/compile-fail/export_not_pub.stderr b/stageleft/tests/compile-fail/export_not_pub.stderr new file mode 100644 index 0000000..6670f04 --- /dev/null +++ b/stageleft/tests/compile-fail/export_not_pub.stderr @@ -0,0 +1,11 @@ +error[E0364]: `NotPubKey` is only public within the crate, and cannot be re-exported outside + --> tests/compile-fail/export_not_pub.rs:1:29 + | +1 | #[stageleft::export(PubKey, NotPubKey)] + | ^^^^^^^^^ + | +note: consider marking `NotPubKey` as `pub` in the imported module + --> tests/compile-fail/export_not_pub.rs:1:29 + | +1 | #[stageleft::export(PubKey, NotPubKey)] + | ^^^^^^^^^ diff --git a/stageleft_test_no_entry/Cargo.toml b/stageleft_test_no_entry/Cargo.toml new file mode 100644 index 0000000..572ed92 --- /dev/null +++ b/stageleft_test_no_entry/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "stageleft_test_no_entry" +version = "0.0.0" +publish = false +edition.workspace = true +license.workspace = true +repository.workspace = true + +[package.metadata.release] +release = false + +[dependencies] +stageleft = { path = "../stageleft", version = "^0.11.0" } + +slotmap = "1.0.0" + +[build-dependencies] +stageleft_tool = { path = "../stageleft_tool", version = "^0.11.0" } diff --git a/stageleft_test_no_entry/build.rs b/stageleft_test_no_entry/build.rs new file mode 100644 index 0000000..1bf2840 --- /dev/null +++ b/stageleft_test_no_entry/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Hack to make sure stageleft actually builds code. + unsafe { std::env::set_var("STAGELEFT_TRYBUILD_BUILD_STAGED", "1") }; + stageleft_tool::gen_final!(); +} diff --git a/stageleft_test_no_entry/src/lib.rs b/stageleft_test_no_entry/src/lib.rs new file mode 100644 index 0000000..e4f8ce1 --- /dev/null +++ b/stageleft_test_no_entry/src/lib.rs @@ -0,0 +1,24 @@ +stageleft::stageleft_no_entry_crate!(); + +stageleft::stageleft_export!(MyKey, OtherKey); + +#[cfg(stageleft_runtime)] +slotmap::new_key_type! { + /// An item generated within a macro. + pub struct MyKey; + + /// Just test the macro expansion is delimiting properly. + pub struct OtherKey; +} + +/// Test that `stageleft::export` prevents splitbrain of `MyKey` type. +#[allow(dead_code)] +fn splitbrain(st: SplitbrainStruct) { + // This gets turned into `crate::__staged::MyKey` + let _key: MyKey = st.my_key; +} + +pub struct SplitbrainStruct { + /// This stays as regular `MyKey` (equiv. to `crate::MyKey`). + my_key: MyKey, +} diff --git a/stageleft_tool/src/lib.rs b/stageleft_tool/src/lib.rs index 3e27a6f..9c80804 100644 --- a/stageleft_tool/src/lib.rs +++ b/stageleft_tool/src/lib.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::{env, fs}; use proc_macro2::Span; -use quote::ToTokens; +use quote::{ToTokens, quote}; use sha2::{Digest, Sha256}; use syn::visit::Visit; use syn::visit_mut::VisitMut; @@ -31,7 +31,7 @@ impl<'a> Visit<'a> for GenMacroVistor { let is_entry = i .attrs .iter() - .any(|a| a.path().to_token_stream().to_string() == "stageleft :: entry"); + .any(|a| a.path().to_token_stream().to_string() == "stageleft :: entry"); // TODO(mingwei): use #root? if is_entry { let cur_path = &self.current_mod; @@ -109,6 +109,7 @@ impl VisitMut for InlineTopLevelMod { i.items.iter_mut().for_each(|i| { if let syn::Item::Macro(e) = i && e.mac.path.to_token_stream().to_string() == "stageleft :: top_level_mod" + // TODO(mingwei): use #root? { let inner = &e.mac.tokens; *i = parse_quote!( @@ -285,7 +286,8 @@ impl VisitMut for GenFinalPubVistor { fn visit_item_mut(&mut self, i: &mut syn::Item) { // TODO(shadaj): warn if a pub struct or enum has private fields // and is not marked for runtime - if let Some(cur_path) = self.current_mod.as_ref() { + let cur_path = self.current_mod.as_ref().unwrap(); + { if let syn::Item::Struct(e) = i { if is_runtime(&e.attrs) { e.attrs.insert( @@ -363,11 +365,11 @@ impl VisitMut for GenFinalPubVistor { } } else if let syn::Item::Macro(m) = i { if is_runtime(&m.attrs) { + // TODO(mingwei): Remove the item entirely (tricky). m.attrs.insert( 0, parse_quote!(#[cfg(all(stageleft_macro, not(stageleft_macro)))]), ); - return; } if m.attrs @@ -383,6 +385,7 @@ impl VisitMut for GenFinalPubVistor { } else if let syn::Item::Impl(e) = i { // TODO(shadaj): emit impls if the struct is private // currently, we just skip all impls + // TODO(mingwei): Remove the item entirely (tricky). *i = parse_quote!( #[cfg(all(stageleft_macro, not(stageleft_macro)))] #e @@ -393,13 +396,40 @@ impl VisitMut for GenFinalPubVistor { syn::visit_mut::visit_item_mut(self, i); } + fn visit_item_macro_mut(&mut self, i: &mut syn::ItemMacro) { + let curr_path = self.current_mod.as_ref().unwrap(); + if i.mac + .path + .segments + .last() + .is_some_and(|m| m.ident == "stageleft_export") + { + match i.mac.parse_body_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + Ok(idents) => { + // Let the macro do the work, via `mod = ...`. + i.mac.tokens = quote! { + mod = #curr_path, #idents + }; + } + Err(err) => { + // `::core::compile_error!("...");` + let compile_err = err.into_compile_error(); + *i = parse_quote!(#compile_err); + } + } + } + syn::visit_mut::visit_item_macro_mut(self, i); + } + fn visit_file_mut(&mut self, i: &mut syn::File) { i.attrs = vec![]; i.items.retain(|i| match i { syn::Item::Macro(m) => { - m.mac.path.to_token_stream().to_string() != "stageleft :: stageleft_crate" + m.mac.path.to_token_stream().to_string() != "stageleft :: stageleft_crate" // TODO(mingwei): use #root? && m.mac.path.to_token_stream().to_string() - != "stageleft :: stageleft_no_entry_crate" + != "stageleft :: stageleft_no_entry_crate" // TODO(mingwei): use #root? } _ => true, });