1use std::collections::BTreeSet;
2use std::path::Path;
3use std::{env, fs};
4
5use proc_macro2::Span;
6use quote::ToTokens;
7use sha2::{Digest, Sha256};
8use syn::parse_quote;
9use syn::visit::Visit;
10use syn::visit_mut::VisitMut;
11use toml_edit::DocumentMut;
12
13struct GenMacroVistor {
14 exported_macros: BTreeSet<(String, String)>,
15 current_mod: syn::Path,
16}
17
18impl<'a> Visit<'a> for GenMacroVistor {
20 fn visit_item_mod(&mut self, i: &'a syn::ItemMod) {
21 self.current_mod.segments.push(i.ident.clone().into());
23
24 syn::visit::visit_item_mod(self, i);
25
26 self.current_mod.segments.pop().unwrap();
28 self.current_mod.segments.pop_punct().unwrap(); }
30
31 fn visit_item_fn(&mut self, i: &'a syn::ItemFn) {
32 let is_entry = i
33 .attrs
34 .iter()
35 .any(|a| a.path().to_token_stream().to_string() == "stageleft :: entry"); if is_entry {
38 let cur_path = &self.current_mod;
39 let mut i_cloned = i.clone();
40 i_cloned.attrs = vec![];
41 i_cloned.vis = syn::Visibility::Inherited; let contents = i_cloned
44 .to_token_stream()
45 .to_string()
46 .chars()
47 .filter(|c| c.is_alphanumeric())
48 .collect::<String>();
49 let contents_hash = format!("{:X}", Sha256::digest(contents));
50 self.exported_macros
51 .insert((contents_hash, cur_path.to_token_stream().to_string()));
52 }
53 }
54}
55
56pub fn gen_macro(staged_path: &Path, crate_name: &str) {
57 let out_dir = env::var_os("OUT_DIR").unwrap();
58 let dest_path = Path::new(&out_dir).join("lib_macro.rs");
59
60 let flow_lib =
61 syn_inline_mod::parse_and_inline_modules(&staged_path.join("src").join("lib.rs"));
62 let mut visitor = GenMacroVistor {
63 exported_macros: Default::default(),
64 current_mod: parse_quote!(crate),
65 };
66 visitor.visit_file(&flow_lib);
67
68 let staged_path_absolute = fs::canonicalize(staged_path).unwrap();
69
70 let mut out_file: syn::File = parse_quote!();
71
72 for (hash, exported_from) in visitor.exported_macros {
73 let underscored_path = syn::Ident::new(&("macro_".to_owned() + &hash), Span::call_site());
74 let underscored_path_impl =
75 syn::Ident::new(&("macro_".to_owned() + &hash + "_impl"), Span::call_site());
76 let exported_from_parsed: syn::Path = syn::parse_str(&exported_from).unwrap();
77
78 let proc_macro_wrapper: syn::ItemFn = parse_quote!(
79 #[proc_macro]
80 #[expect(unused_qualifications, non_snake_case, reason = "generated code")]
81 pub fn #underscored_path(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
82 let input = ::stageleft::internal::TokenStream::from(input);
83 let out = #exported_from_parsed::#underscored_path_impl(input);
84 ::proc_macro::TokenStream::from(out)
85 }
86 );
87
88 out_file.items.push(syn::Item::Fn(proc_macro_wrapper));
89 }
90
91 fs::write(dest_path, prettyplease::unparse(&out_file)).unwrap();
92
93 println!("cargo::rustc-check-cfg=cfg(stageleft_macro)");
94 println!("cargo::rustc-check-cfg=cfg(stageleft_runtime)");
95 println!("cargo::rerun-if-changed=build.rs");
96 println!("cargo::rustc-env=STAGELEFT_FINAL_CRATE_NAME={crate_name}");
97 println!("cargo::rustc-cfg=stageleft_macro");
98
99 println!(
100 "cargo::rerun-if-changed={}",
101 staged_path_absolute.to_string_lossy()
102 );
103}
104
105struct GenFinalPubVisitor {
106 current_mod: syn::Path,
108 stack_is_pub: Vec<bool>,
110
111 test_mode_feature: Option<String>,
114
115 is_staged_separate: bool,
118
119 all_macros: Vec<syn::Ident>,
122}
123impl GenFinalPubVisitor {
124 pub fn new(
125 orig_crate_ident: syn::Path,
126 test_mode_feature: Option<String>,
127 is_staged_separate: bool,
128 ) -> Self {
129 Self {
130 current_mod: orig_crate_ident,
131 stack_is_pub: Vec::new(),
132 test_mode_feature,
133 is_staged_separate,
134 all_macros: Vec::new(),
135 }
136 }
137
138 fn can_access_current(&self) -> bool {
140 self.stack_is_pub
141 .iter()
142 .skip(if self.is_staged_separate { 0 } else { 1 })
145 .all(|&x| x)
146 }
147}
148
149fn get_cfg_attrs(attrs: &[syn::Attribute]) -> impl Iterator<Item = &syn::Attribute> + '_ {
150 attrs.iter().filter(|attr| attr.path().is_ident("cfg"))
151}
152
153fn is_runtime(attrs: &[syn::Attribute]) -> bool {
154 get_cfg_attrs(attrs)
155 .any(|attr| attr.to_token_stream().to_string() == "# [cfg (stageleft_runtime)]")
156}
157
158fn get_stageleft_export_items(attrs: &[syn::Attribute]) -> Option<Vec<syn::Ident>> {
159 attrs
160 .iter()
161 .filter(|a| a.path().to_token_stream().to_string() == "stageleft :: export") .filter_map(|a| {
163 a.parse_args_with(
164 syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated,
165 )
166 .ok()
167 })
168 .fold(None, |mut acc, curr| {
169 acc.get_or_insert_default().extend(curr.iter().cloned());
170 acc
171 })
172}
173
174fn item_attributes(item: &syn::Item) -> &[syn::Attribute] {
175 match item {
176 syn::Item::Const(i) => &i.attrs,
177 syn::Item::Enum(i) => &i.attrs,
178 syn::Item::ExternCrate(i) => &i.attrs,
179 syn::Item::Fn(i) => &i.attrs,
180 syn::Item::ForeignMod(i) => &i.attrs,
181 syn::Item::Impl(i) => &i.attrs,
182 syn::Item::Macro(i) => &i.attrs,
183 syn::Item::Mod(i) => &i.attrs,
184 syn::Item::Struct(i) => &i.attrs,
185 syn::Item::Trait(i) => &i.attrs,
186 syn::Item::Type(i) => &i.attrs,
187 syn::Item::Union(i) => &i.attrs,
188 syn::Item::Use(i) => &i.attrs,
189 syn::Item::Static(i) => &i.attrs,
190 syn::Item::TraitAlias(i) => &i.attrs,
191 syn::Item::Verbatim(_) => &[],
192 x => panic!("Unknown item type: {:?}", x),
193 }
194}
195
196fn item_visibility_ident(item: &syn::Item) -> Option<(&syn::Visibility, &syn::Ident)> {
197 match item {
198 syn::Item::Const(i) => Some((&i.vis, &i.ident)),
199 syn::Item::Enum(i) => Some((&i.vis, &i.ident)),
200 syn::Item::Fn(i) => Some((&i.vis, &i.sig.ident)),
201 syn::Item::Static(i) => Some((&i.vis, &i.ident)),
202 syn::Item::Struct(i) => Some((&i.vis, &i.ident)),
203 syn::Item::Trait(i) => Some((&i.vis, &i.ident)),
204 syn::Item::TraitAlias(i) => Some((&i.vis, &i.ident)),
205 syn::Item::Type(i) => Some((&i.vis, &i.ident)),
206 syn::Item::Union(i) => Some((&i.vis, &i.ident)),
207 _ => None,
208 }
209}
210
211impl VisitMut for GenFinalPubVisitor {
212 fn visit_item_enum_mut(&mut self, i: &mut syn::ItemEnum) {
213 i.vis = parse_quote!(pub);
214 syn::visit_mut::visit_item_enum_mut(self, i);
215 }
216
217 fn visit_variant_mut(&mut self, _i: &mut syn::Variant) {
218 }
220
221 fn visit_item_static_mut(&mut self, i: &mut syn::ItemStatic) {
222 i.vis = parse_quote!(pub);
223 syn::visit_mut::visit_item_static_mut(self, i);
224 }
225
226 fn visit_item_const_mut(&mut self, i: &mut syn::ItemConst) {
227 i.vis = parse_quote!(pub);
228 syn::visit_mut::visit_item_const_mut(self, i);
229 }
230
231 fn visit_item_struct_mut(&mut self, i: &mut syn::ItemStruct) {
232 i.vis = parse_quote!(pub);
233 syn::visit_mut::visit_item_struct_mut(self, i);
234 }
235
236 fn visit_item_type_mut(&mut self, i: &mut syn::ItemType) {
237 i.vis = parse_quote!(pub);
238 syn::visit_mut::visit_item_type_mut(self, i);
239 }
240
241 fn visit_field_mut(&mut self, i: &mut syn::Field) {
242 i.vis = parse_quote!(pub);
243 syn::visit_mut::visit_field_mut(self, i);
244 }
245
246 fn visit_item_use_mut(&mut self, i: &mut syn::ItemUse) {
247 i.vis = parse_quote!(pub);
248 syn::visit_mut::visit_item_use_mut(self, i);
249 }
250
251 fn visit_use_path_mut(&mut self, i: &mut syn::UsePath) {
252 if i.ident == "crate" {
253 *i.tree = syn::UseTree::Path(syn::UsePath {
254 ident: parse_quote!(__staged),
255 colon2_token: Default::default(),
256 tree: i.tree.clone(),
257 });
258 }
259
260 syn::visit_mut::visit_use_path_mut(self, i);
261 }
262
263 fn visit_vis_restricted_mut(&mut self, _i: &mut syn::VisRestricted) {
264 }
266
267 fn visit_path_mut(&mut self, i: &mut syn::Path) {
268 if !i.segments.is_empty() && i.segments[0].ident == "crate" {
269 i.segments.insert(
270 1,
271 syn::PathSegment {
272 ident: parse_quote!(__staged),
273 arguments: Default::default(),
274 },
275 );
276 }
277
278 syn::visit_mut::visit_path_mut(self, i);
279 }
280
281 fn visit_item_mod_mut(&mut self, i: &mut syn::ItemMod) {
282 self.current_mod.segments.push(i.ident.clone().into());
284 self.stack_is_pub
285 .push(matches!(i.vis, syn::Visibility::Public(_)));
286
287 syn::visit_mut::visit_item_mod_mut(self, i);
288
289 self.current_mod.segments.pop().unwrap();
291 self.current_mod.segments.pop_punct().unwrap(); self.stack_is_pub.pop().unwrap();
293
294 i.vis = parse_quote!(pub);
296 }
297
298 fn visit_item_fn_mut(&mut self, i: &mut syn::ItemFn) {
299 i.vis = parse_quote!(pub);
300 syn::visit_mut::visit_item_fn_mut(self, i);
301 }
302
303 fn visit_item_mut(&mut self, i: &mut syn::Item) {
304 let cur_path = &self.current_mod;
307
308 if is_runtime(item_attributes(i)) {
310 *i = syn::Item::Verbatim(Default::default());
311 return;
312 }
313
314 match i {
315 syn::Item::Macro(m) => {
316 if let Some(exported_items) = get_stageleft_export_items(&m.attrs) {
318 *i = parse_quote! {
319 pub use #cur_path::{ #( #exported_items ),* };
320 };
321 return;
322 }
323
324 if m.attrs
325 .iter()
326 .any(|a| a.to_token_stream().to_string() == "# [macro_export]")
327 {
328 self.all_macros.push(m.ident.as_ref().unwrap().clone());
330 *i = syn::Item::Verbatim(Default::default());
331 return;
332 }
333 }
334 syn::Item::Impl(_e) => {
335 *i = syn::Item::Verbatim(Default::default());
338 return;
339 }
340 syn::Item::Mod(m) => {
341 let is_test_mod = m
342 .attrs
343 .iter()
344 .any(|a| a.to_token_stream().to_string() == "# [cfg (test)]");
345
346 if is_test_mod {
347 m.attrs
348 .retain(|a| a.to_token_stream().to_string() != "# [cfg (test)]");
349
350 if let Some(feature) = &self.test_mode_feature {
351 m.attrs.insert(0, parse_quote!(#[cfg(feature = #feature)]));
352 } else {
353 *i = syn::Item::Verbatim(Default::default());
356 return;
357 }
358 }
359 }
360 syn::Item::Fn(f) => {
361 let is_ctor = f
362 .attrs
363 .iter()
364 .any(|a| a.path().to_token_stream().to_string() == "ctor :: ctor");
365
366 let is_test = f.attrs.iter().any(|a| {
367 a.path().to_token_stream().to_string() == "test"
368 || a.path().to_token_stream().to_string() == "tokio :: test"
369 });
370
371 if is_ctor || is_test {
372 *i = syn::Item::Verbatim(Default::default());
374 return;
375 }
376 }
377 _ => {}
378 }
379
380 if self.can_access_current()
382 && let Some((syn::Visibility::Public(_), name_ident)) = item_visibility_ident(i)
383 {
384 let cfg_attrs = get_cfg_attrs(item_attributes(i));
385 *i = parse_quote!(#(#cfg_attrs)* pub use #cur_path::#name_ident;);
386 return;
387 }
388
389 syn::visit_mut::visit_item_mut(self, i);
390 }
391
392 fn visit_file_mut(&mut self, i: &mut syn::File) {
393 i.attrs = vec![];
394 i.items.retain(|i| match i {
395 syn::Item::Macro(m) => {
396 m.mac.path.to_token_stream().to_string() != "stageleft :: stageleft_crate" && m.mac.path.to_token_stream().to_string()
398 != "stageleft :: stageleft_no_entry_crate" }
400 _ => true,
401 });
402
403 syn::visit_mut::visit_file_mut(self, i);
404 }
405}
406
407fn gen_deps_module(stageleft_name: syn::Ident, manifest_path: &Path) -> syn::ItemMod {
408 let toml_parsed = fs::read_to_string(manifest_path)
410 .unwrap()
411 .parse::<DocumentMut>()
412 .unwrap();
413 let all_crate_names = toml_parsed["dependencies"]
414 .as_table()
415 .unwrap()
416 .iter()
417 .filter(|(_, v)| !v.get("optional").and_then(|o| o.as_bool()).unwrap_or(false))
418 .map(|(name, v)| {
419 (
420 name.replace('-', "_"),
421 v.get("package")
422 .map(|v| v.as_str().unwrap().replace("-", "_")),
423 )
424 })
425 .collect::<Vec<_>>();
426
427 let deps_reexported = all_crate_names
428 .iter()
429 .map(|(name, _)| {
430 let name_ident = syn::Ident::new(name, Span::call_site());
431 parse_quote! {
432 pub use #name_ident;
433 }
434 })
435 .collect::<Vec<syn::Item>>();
436
437 let deps_reexported_runtime = all_crate_names
438 .iter()
439 .map(|(name, original_crate_name)| {
440 let original_crate_name_or_alias = original_crate_name.as_deref().unwrap_or(name);
441 parse_quote! {
442 #stageleft_name::internal::add_deps_reexport(
443 vec![#original_crate_name_or_alias],
444 vec![
445 option_env!("STAGELEFT_FINAL_CRATE_NAME")
446 .unwrap_or(env!("CARGO_PKG_NAME"))
447 .replace("-", "_"),
448 ::std::borrow::ToOwned::to_owned("__staged"),
449 ::std::borrow::ToOwned::to_owned("__deps"),
450 ::std::borrow::ToOwned::to_owned(#name),
451 ]
452 );
453 }
454 })
455 .collect::<Vec<syn::Stmt>>();
456
457 syn::parse_quote! {
458 pub mod __deps {
459 #(#deps_reexported)*
460
461 #[#stageleft_name::internal::ctor::ctor(crate_path = #stageleft_name::internal::ctor)]
462 fn __init() {
463 #(#deps_reexported_runtime)*
464 #stageleft_name::internal::add_crate_with_staged(env!("CARGO_PKG_NAME").replace("-", "_"));
465 }
466 }
467 }
468}
469
470fn gen_staged_mod(
482 lib_path: &Path,
483 orig_crate_path: syn::Path,
484 test_mode_feature: Option<String>,
485 is_staged_separate: bool,
486) -> syn::File {
487 assert!(
488 !orig_crate_path.segments.trailing_punct(),
489 "`orig_crate_path` may not have trailing `::`"
490 );
491
492 let mut flow_lib_pub = syn_inline_mod::parse_and_inline_modules(lib_path);
493
494 let mut final_pub_visitor = GenFinalPubVisitor::new(
495 orig_crate_path.clone(),
496 test_mode_feature,
497 is_staged_separate,
498 );
499 final_pub_visitor.visit_file_mut(&mut flow_lib_pub);
500
501 for exported_macro in final_pub_visitor.all_macros {
505 flow_lib_pub
506 .items
507 .push(parse_quote!(pub use #orig_crate_path::#exported_macro;));
508 }
509
510 flow_lib_pub
511}
512
513pub fn gen_staged_trybuild(
525 lib_path: &Path,
526 manifest_path: &Path,
527 orig_crate_path: &str,
528 test_mode_feature: Option<String>,
529) -> syn::File {
530 let orig_crate_path = syn::parse_str(orig_crate_path)
531 .expect("Failed to parse `orig_crate_path` as `crate`, crate name, or module path.");
532 let mut flow_lib_pub = gen_staged_mod(lib_path, orig_crate_path, test_mode_feature, true);
533
534 let deps_mod = gen_deps_module(parse_quote!(stageleft), manifest_path);
535
536 flow_lib_pub.items.push(syn::Item::Mod(deps_mod));
537 flow_lib_pub
538}
539
540#[doc(hidden)]
541pub fn gen_staged_pub() {
542 let out_dir = env::var_os("OUT_DIR").unwrap();
543
544 let raw_toml_manifest = fs::read_to_string(Path::new("Cargo.toml"))
545 .unwrap()
546 .parse::<DocumentMut>()
547 .unwrap();
548
549 let maybe_custom_lib_path = raw_toml_manifest
550 .get("lib")
551 .and_then(|lib| lib.get("path"))
552 .and_then(|path| path.as_str());
553
554 let flow_lib_pub = gen_staged_mod(
555 maybe_custom_lib_path
556 .map(Path::new)
557 .unwrap_or_else(|| Path::new("src/lib.rs")),
558 parse_quote!(crate),
559 None,
560 false,
561 );
562
563 fs::write(
564 Path::new(&out_dir).join("lib_pub.rs"),
565 prettyplease::unparse(&flow_lib_pub),
566 )
567 .unwrap();
568 println!("cargo::rerun-if-changed=src");
569}
570
571#[doc(hidden)]
572pub fn gen_staged_deps() {
573 let out_dir = env::var_os("OUT_DIR").unwrap();
574
575 let main_pkg_name = env!("CARGO_PKG_NAME").rsplit_once(['-', '_']).unwrap().0;
577 let stageleft_crate = proc_macro_crate::crate_name(main_pkg_name).unwrap_or_else(|_| {
578 panic!("Expected stageleft {main_pkg_name} package to be present in `Cargo.toml`")
579 });
580 let stageleft_name = match stageleft_crate {
581 proc_macro_crate::FoundCrate::Itself => syn::Ident::new(main_pkg_name, Span::call_site()),
582 proc_macro_crate::FoundCrate::Name(name) => syn::Ident::new(&name, Span::call_site()),
583 };
584
585 let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
586 let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
587
588 let deps_file = gen_deps_module(stageleft_name, &manifest_path);
589
590 fs::write(
591 Path::new(&out_dir).join("staged_deps.rs"),
592 prettyplease::unparse(&parse_quote!(#deps_file)),
593 )
594 .unwrap();
595}
596
597#[macro_export]
598macro_rules! gen_final {
599 () => {
600 println!("cargo::rustc-check-cfg=cfg(stageleft_macro)");
601 println!("cargo::rustc-check-cfg=cfg(stageleft_runtime)");
602 println!("cargo::rustc-check-cfg=cfg(stageleft_trybuild)");
603 println!("cargo::rustc-check-cfg=cfg(feature, values(\"stageleft_macro_entrypoint\"))");
604 println!("cargo::rustc-cfg=stageleft_runtime");
605
606 println!("cargo::rerun-if-changed=Cargo.toml");
607 println!("cargo::rerun-if-changed=build.rs");
608 println!("cargo::rerun-if-env-changed=STAGELEFT_TRYBUILD_BUILD_STAGED");
609
610 #[allow(
611 unexpected_cfgs,
612 reason = "Macro entrypoints must define the stageleft_macro_entrypoint feature"
613 )]
614 {
615 if cfg!(feature = "stageleft_macro_entrypoint") {
616 $crate::gen_staged_pub()
617 } else if std::env::var("STAGELEFT_TRYBUILD_BUILD_STAGED").is_ok() {
618 println!("cargo::rustc-cfg=stageleft_trybuild");
619 $crate::gen_staged_pub()
620 }
621 }
622
623 $crate::gen_staged_deps()
624 };
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use std::io::Write;
631 use tempfile::NamedTempFile;
632
633 #[test]
634 fn test_gen_deps_module_uses_crate_name_or_alias() {
635 let mut temp_file = NamedTempFile::new().unwrap();
637 writeln!(temp_file, "[dependencies]").unwrap();
638 writeln!(
639 temp_file,
640 r#"my_alias = {{ package = "actual_crate", version = "1.0" }}"#
641 )
642 .unwrap();
643 writeln!(temp_file, r#"regular_crate = "2.0""#).unwrap();
644 temp_file.flush().unwrap();
645
646 let stageleft_name = syn::Ident::new("stageleft", Span::call_site());
647 let deps_module = gen_deps_module(stageleft_name, temp_file.path());
648
649 let generated_code = quote::quote!(#deps_module).to_string();
650
651 assert!(
652 generated_code.contains(r#""actual_crate""#),
653 "Generated code should use actual crate name for aliased dependency: {}",
654 generated_code
655 );
656
657 assert!(
658 generated_code.contains(r#""regular_crate""#),
659 "Generated code should use dependency name for regular dependency: {}",
660 generated_code
661 );
662
663 assert!(
664 generated_code.contains("add_deps_reexport"),
665 "Generated code should contain add_deps_reexport calls: {}",
666 generated_code
667 );
668 }
669}