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::visit::Visit;
9use syn::visit_mut::VisitMut;
10use syn::{parse_quote, UsePath};
11
12struct GenMacroVistor {
13    exported_macros: BTreeSet<(String, String)>,
14    current_mod: syn::Path,
15}
16
17// marks everything as pub(crate) because proc-macros cannot actually export anything
18impl<'a> Visit<'a> for GenMacroVistor {
19    fn visit_item_mod(&mut self, i: &'a syn::ItemMod) {
20        let old_mod = self.current_mod.clone();
21        let i_ident = &i.ident;
22        self.current_mod = parse_quote!(#old_mod::#i_ident);
23
24        syn::visit::visit_item_mod(self, i);
25
26        self.current_mod = old_mod;
27    }
28
29    fn visit_item_fn(&mut self, i: &'a syn::ItemFn) {
30        let is_entry = i
31            .attrs
32            .iter()
33            .any(|a| a.path().to_token_stream().to_string() == "stageleft :: entry");
34
35        if is_entry {
36            let cur_path = &self.current_mod;
37            let mut i_cloned = i.clone();
38            i_cloned.attrs = vec![];
39            let contents = i_cloned
40                .to_token_stream()
41                .to_string()
42                .chars()
43                .filter(|c| c.is_alphanumeric())
44                .collect::<String>();
45            let contents_hash = format!("{:X}", Sha256::digest(contents));
46            self.exported_macros
47                .insert((contents_hash, cur_path.to_token_stream().to_string()));
48        }
49    }
50}
51
52pub fn gen_macro(staged_path: &Path, crate_name: &str) {
53    let out_dir = env::var_os("OUT_DIR").unwrap();
54    let dest_path = Path::new(&out_dir).join("lib_macro.rs");
55
56    let flow_lib =
57        syn_inline_mod::parse_and_inline_modules(&staged_path.join("src").join("lib.rs"));
58    let mut visitor = GenMacroVistor {
59        exported_macros: Default::default(),
60        current_mod: parse_quote!(crate),
61    };
62    visitor.visit_file(&flow_lib);
63
64    let staged_path_absolute = fs::canonicalize(staged_path).unwrap();
65
66    let mut out_file: syn::File = parse_quote!();
67
68    for (hash, exported_from) in visitor.exported_macros {
69        let underscored_path = syn::Ident::new(&("macro_".to_string() + &hash), Span::call_site());
70        let underscored_path_impl =
71            syn::Ident::new(&("macro_".to_string() + &hash + "_impl"), Span::call_site());
72        let exported_from_parsed: syn::Path = syn::parse_str(&exported_from).unwrap();
73
74        let proc_macro_wrapper: syn::ItemFn = parse_quote!(
75            #[proc_macro]
76            #[expect(unused_qualifications, non_snake_case, reason = "generated code")]
77            pub fn #underscored_path(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
78                let input = ::stageleft::internal::TokenStream::from(input);
79                let out = #exported_from_parsed::#underscored_path_impl(input);
80                ::proc_macro::TokenStream::from(out)
81            }
82        );
83
84        out_file.items.push(syn::Item::Fn(proc_macro_wrapper));
85    }
86
87    fs::write(dest_path, out_file.to_token_stream().to_string()).unwrap();
88
89    println!("cargo::rustc-check-cfg=cfg(stageleft_macro)");
90    println!("cargo::rerun-if-changed=build.rs");
91    println!("cargo::rustc-env=STAGELEFT_FINAL_CRATE_NAME={}", crate_name);
92    println!("cargo::rustc-cfg=stageleft_macro");
93
94    println!(
95        "cargo::rerun-if-changed={}",
96        staged_path_absolute.to_string_lossy()
97    );
98}
99
100struct InlineTopLevelMod {}
101
102impl VisitMut for InlineTopLevelMod {
103    fn visit_file_mut(&mut self, i: &mut syn::File) {
104        i.attrs = vec![];
105        i.items.iter_mut().for_each(|i| {
106            if let syn::Item::Macro(e) = i {
107                if e.mac.path.to_token_stream().to_string() == "stageleft :: top_level_mod" {
108                    let inner = &e.mac.tokens;
109                    *i = parse_quote!(
110                        pub mod #inner;
111                    );
112                }
113            }
114        });
115    }
116}
117
118struct GenFinalPubVistor {
119    current_mod: Option<syn::Path>,
120    test_mode: bool,
121}
122
123impl VisitMut for GenFinalPubVistor {
124    fn visit_item_enum_mut(&mut self, i: &mut syn::ItemEnum) {
125        i.vis = parse_quote!(pub);
126        syn::visit_mut::visit_item_enum_mut(self, i);
127    }
128
129    fn visit_variant_mut(&mut self, _i: &mut syn::Variant) {
130        // variant fields do not have visibility modifiers
131    }
132
133    fn visit_item_struct_mut(&mut self, i: &mut syn::ItemStruct) {
134        i.vis = parse_quote!(pub);
135        syn::visit_mut::visit_item_struct_mut(self, i);
136    }
137
138    fn visit_field_mut(&mut self, i: &mut syn::Field) {
139        i.vis = parse_quote!(pub);
140        syn::visit_mut::visit_field_mut(self, i);
141    }
142
143    fn visit_item_use_mut(&mut self, i: &mut syn::ItemUse) {
144        i.vis = parse_quote!(pub);
145        syn::visit_mut::visit_item_use_mut(self, i);
146    }
147
148    fn visit_use_path_mut(&mut self, i: &mut UsePath) {
149        if i.ident == "crate" {
150            i.tree = Box::new(syn::UseTree::Path(UsePath {
151                ident: parse_quote!(__staged),
152                colon2_token: Default::default(),
153                tree: i.tree.clone(),
154            }));
155        }
156
157        syn::visit_mut::visit_use_path_mut(self, i);
158    }
159
160    fn visit_item_mod_mut(&mut self, i: &mut syn::ItemMod) {
161        let is_runtime_or_test = i.attrs.iter().any(|a| {
162            a.path().to_token_stream().to_string() == "stageleft :: runtime"
163                || a.to_token_stream().to_string() == "# [test]"
164                || a.to_token_stream().to_string() == "# [tokio::test]"
165        });
166
167        let is_test_mod = i
168            .attrs
169            .iter()
170            .any(|a| a.to_token_stream().to_string() == "# [cfg (test)]");
171
172        if is_runtime_or_test {
173            *i = parse_quote! {
174                #[cfg(stageleft_macro)]
175                #i
176            };
177        } else {
178            if is_test_mod {
179                i.attrs
180                    .retain(|a| a.to_token_stream().to_string() != "# [cfg (test)]");
181
182                if !self.test_mode {
183                    i.attrs.push(parse_quote!(#[cfg(stageleft_macro)]));
184                }
185            }
186
187            let old_mod = self.current_mod.clone();
188            let i_ident = &i.ident;
189            self.current_mod = self
190                .current_mod
191                .as_ref()
192                .map(|old_mod| parse_quote!(#old_mod::#i_ident));
193
194            i.vis = parse_quote!(pub);
195
196            syn::visit_mut::visit_item_mod_mut(self, i);
197
198            self.current_mod = old_mod;
199        }
200    }
201
202    fn visit_item_fn_mut(&mut self, i: &mut syn::ItemFn) {
203        let is_entry = i
204            .attrs
205            .iter()
206            .any(|a| a.path().to_token_stream().to_string() == "stageleft :: entry");
207
208        if is_entry {
209            *i = parse_quote! {
210                #[cfg(stageleft_macro)]
211                #i
212            }
213        }
214
215        i.vis = parse_quote!(pub);
216
217        syn::visit_mut::visit_item_fn_mut(self, i);
218    }
219
220    fn visit_item_mut(&mut self, i: &mut syn::Item) {
221        // TODO(shadaj): warn if a pub struct or enum has private fields
222        // and is not marked for runtime
223        if let Some(cur_path) = self.current_mod.as_ref() {
224            if let syn::Item::Struct(s) = i {
225                if matches!(s.vis, syn::Visibility::Public(_)) {
226                    let e_name = &s.ident;
227                    *i = parse_quote!(pub use #cur_path::#e_name;);
228                    return;
229                }
230            } else if let syn::Item::Enum(e) = i {
231                if matches!(e.vis, syn::Visibility::Public(_)) {
232                    let e_name = &e.ident;
233                    *i = parse_quote!(pub use #cur_path::#e_name;);
234                    return;
235                }
236            } else if let syn::Item::Trait(e) = i {
237                if matches!(e.vis, syn::Visibility::Public(_)) {
238                    let e_name = &e.ident;
239                    *i = parse_quote!(pub use #cur_path::#e_name;);
240                    return;
241                }
242            } else if let syn::Item::Impl(e) = i {
243                // TODO(shadaj): emit impls if the struct is private
244                *i = parse_quote!(
245                    #[cfg(stageleft_macro)]
246                    #e
247                );
248            } else if let syn::Item::Static(e) = i {
249                if matches!(e.vis, syn::Visibility::Public(_)) {
250                    let e_name = &e.ident;
251                    *i = parse_quote!(pub use #cur_path::#e_name;);
252                    return;
253                }
254            } else if let syn::Item::Const(e) = i {
255                if matches!(e.vis, syn::Visibility::Public(_)) {
256                    let e_name = &e.ident;
257                    *i = parse_quote!(pub use #cur_path::#e_name;);
258                    return;
259                }
260            }
261        }
262
263        syn::visit_mut::visit_item_mut(self, i);
264    }
265
266    fn visit_file_mut(&mut self, i: &mut syn::File) {
267        i.attrs = vec![];
268        i.items.retain(|i| match i {
269            syn::Item::Macro(m) => {
270                m.mac.path.to_token_stream().to_string() != "stageleft :: stageleft_crate"
271                    && m.mac.path.to_token_stream().to_string()
272                        != "stageleft :: stageleft_no_entry_crate"
273            }
274            _ => true,
275        });
276
277        syn::visit_mut::visit_file_mut(self, i);
278    }
279}
280
281pub fn gen_staged_trybuild(lib_path: &Path, orig_crate_name: String, test_mode: bool) -> syn::File {
282    let mut orig_flow_lib = syn_inline_mod::parse_and_inline_modules(lib_path);
283    InlineTopLevelMod {}.visit_file_mut(&mut orig_flow_lib);
284
285    let mut flow_lib_pub = syn_inline_mod::parse_and_inline_modules(lib_path);
286
287    let orig_crate_ident = syn::Ident::new(&orig_crate_name, Span::call_site());
288    let mut final_pub_visitor = GenFinalPubVistor {
289        current_mod: Some(parse_quote!(#orig_crate_ident)),
290        test_mode,
291    };
292    final_pub_visitor.visit_file_mut(&mut flow_lib_pub);
293
294    flow_lib_pub
295}
296
297pub fn gen_final_helper() {
298    let out_dir = env::var_os("OUT_DIR").unwrap();
299
300    let mut orig_flow_lib = syn_inline_mod::parse_and_inline_modules(Path::new("src/lib.rs"));
301    InlineTopLevelMod {}.visit_file_mut(&mut orig_flow_lib);
302
303    let mut flow_lib_pub = syn_inline_mod::parse_and_inline_modules(Path::new("src/lib.rs"));
304
305    let mut final_pub_visitor = GenFinalPubVistor {
306        current_mod: Some(parse_quote!(crate)),
307        test_mode: false,
308    };
309    final_pub_visitor.visit_file_mut(&mut flow_lib_pub);
310
311    fs::write(
312        Path::new(&out_dir).join("lib_pub.rs"),
313        flow_lib_pub.to_token_stream().to_string(),
314    )
315    .unwrap();
316
317    println!("cargo::rustc-check-cfg=cfg(stageleft_macro)");
318    println!("cargo::rerun-if-changed=build.rs");
319    println!("cargo::rerun-if-changed=src");
320}
321
322#[macro_export]
323macro_rules! gen_final {
324    () => {
325        #[cfg(not(feature = "stageleft_devel"))]
326        $crate::gen_final_helper()
327    };
328}