Skip to main content

stageleft_tool/
lib.rs

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
18// marks everything as pub(crate) because proc-macros cannot actually export anything
19impl<'a> Visit<'a> for GenMacroVistor {
20    fn visit_item_mod(&mut self, i: &'a syn::ItemMod) {
21        // Push
22        self.current_mod.segments.push(i.ident.clone().into());
23
24        syn::visit::visit_item_mod(self, i);
25
26        // Pop
27        self.current_mod.segments.pop().unwrap();
28        self.current_mod.segments.pop_punct().unwrap(); // Remove trailing `::`.
29    }
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"); // TODO(mingwei): use #root?
36
37        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; // normalize pub
42
43            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    /// The current module path, starting with `crate`.
107    current_mod: syn::Path,
108    /// Stack of if each segment of `current_mod` is pub.
109    stack_is_pub: Vec<bool>,
110
111    /// If `Some("FEATURE")`, `#[cfg(test)]` modules will be gated with `#[cfg(feature = "FEATURE")]` instead of being
112    /// fully removed.
113    test_mode_feature: Option<String>,
114
115    /// Whether the staged crate will be included in a separate crate (instead of the original crate as is usual). If
116    /// true, then disables `pub use` [re-exporting from non-`pub` ancestor modules](https://doc.rust-lang.org/reference/visibility-and-privacy.html#r-vis.access).
117    is_staged_separate: bool,
118
119    /// All `#[macro_export]` declarative macros encountered, to be re-exported at the top `__staged` module due to the
120    /// strange way `#[macro_export]` works.
121    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    /// If items in the current module path ([`Self::current_mod`]) are accessible, for `pub use` re-exporting.
139    fn can_access_current(&self) -> bool {
140        self.stack_is_pub
141            .iter()
142            // If the staged crate is included in the original crate, the innermost module may be private due to the
143            // ancestor rule: https://doc.rust-lang.org/reference/visibility-and-privacy.html#r-vis.access
144            .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") // TODO(mingwei): use #root?
162        .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        // variant fields do not have visibility modifiers
219    }
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        // don't treat the restriction as a path, we don't want to rewrite that to `__staged`
265    }
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        // Push
283        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        // Pop
290        self.current_mod.segments.pop().unwrap();
291        self.current_mod.segments.pop_punct().unwrap(); // Remove trailing `::`.
292        self.stack_is_pub.pop().unwrap();
293
294        // Make module pub.
295        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        // TODO(shadaj): warn if a pub struct or enum has private fields
305        // and is not marked for runtime
306        let cur_path = &self.current_mod;
307
308        // Remove if marked with `#[cfg(stageleft_runtime)]`
309        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                // TODO(mingwei): Handle if `can_access_current()` is false
317                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                    // Re-export macro at top-level later.
329                    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                // TODO(shadaj): emit impls if the **struct** is private
336                // currently, we just skip all impls
337                *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                        // if test mode is not enabled, there are no quoted snippets behind #[cfg(test)],
354                        // so no #[cfg(test)] modules will ever be reachable
355                        *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                    // no quoted code depends on this module, so we do not need to copy it
373                    *i = syn::Item::Verbatim(Default::default());
374                    return;
375                }
376            }
377            _ => {}
378        }
379
380        // If a named item can be accessed (mod can be accessed and item is pub), simply re-export from original crate.
381        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" // TODO(mingwei): use #root?
397                    && m.mac.path.to_token_stream().to_string()
398                        != "stageleft :: stageleft_no_entry_crate" // TODO(mingwei): use #root?
399            }
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    // based on proc-macro-crate
409    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
470/// Generates the contents of `mod __staged`, which contains a copy of the crate's code but with
471/// all APIs made public so they can be resolved when quoted code is spliced.
472///
473/// # Arguments
474/// * `lib_path` - path to the root Rust file, usually to `lib.rs`.
475/// * `orig_crate_path` - Rust module path to the staged crate. Usually `crate`, but may be the staged crate name if
476///   the entry and staged crate/target are different.
477/// * `is_staged_separate` - Whether the staged crate will be included in a separate crate (instead of the original
478///   crate as is usual). If true, then disables `pub use` [re-exporting from non-`pub` ancestor modules](https://doc.rust-lang.org/reference/visibility-and-privacy.html#r-vis.access).
479/// * `test_mode_feature` - If `Some("FEATURE")`, `#[cfg(test)]` modules will be gated with
480///   `#[cfg(feature = "FEATURE")]` instead of being fully removed.
481fn 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    // macros exported with `#[macro_export]` are placed at the top-level of the crate,
502    // so we need to pull them into the `mod __staged` so that relative imports resolve
503    // correctly
504    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
513/// Generates the contents for `__staged` when it will be emitted in "trybuild mode", which means that
514/// it is included inline next to the spliced code that uses it, with the original crate available as
515/// a dependency.
516///
517/// # Arguments
518/// * `lib_path` - path to the root Rust file, usually to `lib.rs`.
519/// * `manifest_path` - path to the package `Cargo.toml`.
520/// * `orig_crate_path` - Rust module path to the staged crate. Usually `crate`, but may be the staged crate name if
521///   the entry and staged crate/target are different.
522/// * `test_mode_feature` - If `Some("FEATURE")`, `#[cfg(test)]` modules will be gated with
523///   `#[cfg(feature = "FEATURE")]` instead of being fully removed.
524pub 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    // Remove `_tool` suffix.
576    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        // Create a temporary Cargo.toml with a dependency that has a package alias
636        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}