Skip to content

Commit b193be1

Browse files
fix(stageleft_tool): track is_staged_separate, pubness of entire module stack, re-export static & trait alias items (#54)
This works for current functionality. Note the nuanced implementation of `can_access_current`, which varies depending on if the staged module is included in the original crate or separately. This is due to the [ancestor rule](https://doc.rust-lang.org/reference/visibility-and-privacy.html#r-vis.access). --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2a8ab3f commit b193be1

File tree

4 files changed

+166
-31
lines changed

4 files changed

+166
-31
lines changed

stageleft_test/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ mod private {
1818
) -> crate::private::SomeType {
1919
123
2020
}
21+
22+
mod extra_private {
23+
#[allow(dead_code)]
24+
pub struct PubInsideExtraPrivate;
25+
}
2126
}
2227

2328
#[stageleft::entry]

stageleft_test_no_entry/src/lib.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,39 @@ pub struct SplitbrainStruct {
2222
#[expect(dead_code)]
2323
struct ThisShouldBeRemoved;
2424

25-
pub mod pub_mod {
25+
#[allow(dead_code)]
26+
pub mod public {
2627
#[cfg(stageleft_runtime)]
27-
#[expect(dead_code)]
2828
struct ThisShouldAlsoBeRemoved;
29+
30+
pub fn f() {}
31+
fn g() {}
32+
33+
#[expect(clippy::module_inception)]
34+
pub mod public {
35+
pub fn f() {}
36+
fn g() {}
37+
}
38+
39+
mod private {
40+
pub fn f() {}
41+
fn g() {}
42+
}
43+
}
44+
45+
#[allow(dead_code)]
46+
mod private {
47+
pub fn f() {}
48+
fn g() {}
49+
50+
pub mod public {
51+
pub fn f() {}
52+
fn g() {}
53+
}
54+
55+
#[expect(clippy::module_inception)]
56+
mod private {
57+
pub fn f() {}
58+
fn g() {}
59+
}
2960
}

stageleft_test_no_entry/tests/snapshots/codegen_snapshot__lib_pub.snap

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,32 @@ pub fn splitbrain(st: SplitbrainStruct) {
1010
}
1111
pub use crate::SplitbrainStruct;
1212

13-
pub mod pub_mod {
13+
#[allow(dead_code)]
14+
pub mod public {
1415

16+
pub use crate::public::f;
17+
pub fn g() {}
18+
#[expect(clippy::module_inception)]
19+
pub mod public {
20+
pub use crate::public::public::f;
21+
pub fn g() {}
22+
}
23+
pub mod private {
24+
pub fn f() {}
25+
pub fn g() {}
26+
}
27+
}
28+
#[allow(dead_code)]
29+
pub mod private {
30+
pub use crate::private::f;
31+
pub fn g() {}
32+
pub mod public {
33+
pub use crate::private::public::f;
34+
pub fn g() {}
35+
}
36+
#[expect(clippy::module_inception)]
37+
pub mod private {
38+
pub fn f() {}
39+
pub fn g() {}
40+
}
1541
}

stageleft_tool/src/lib.rs

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ struct GenMacroVistor {
1818
// marks everything as pub(crate) because proc-macros cannot actually export anything
1919
impl<'a> Visit<'a> for GenMacroVistor {
2020
fn visit_item_mod(&mut self, i: &'a syn::ItemMod) {
21-
let old_mod = self.current_mod.clone();
22-
let i_ident = &i.ident;
23-
self.current_mod = parse_quote!(#old_mod::#i_ident);
21+
// Push
22+
self.current_mod.segments.push(i.ident.clone().into());
2423

2524
syn::visit::visit_item_mod(self, i);
2625

27-
self.current_mod = old_mod;
26+
// Pop
27+
self.current_mod.segments.pop().unwrap();
28+
self.current_mod.segments.pop_punct().unwrap(); // Remove trailing `::`.
2829
}
2930

3031
fn visit_item_fn(&mut self, i: &'a syn::ItemFn) {
@@ -102,9 +103,47 @@ pub fn gen_macro(staged_path: &Path, crate_name: &str) {
102103
}
103104

104105
struct GenFinalPubVisitor {
105-
current_mod: Option<syn::Path>,
106-
all_macros: Vec<syn::Ident>,
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.
107113
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+
}
123+
impl 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+
}
108147
}
109148

110149
fn get_cfg_attrs(attrs: &[syn::Attribute]) -> impl Iterator<Item = &syn::Attribute> + '_ {
@@ -159,8 +198,10 @@ fn item_visibility_ident(item: &syn::Item) -> Option<(&syn::Visibility, &syn::Id
159198
syn::Item::Const(i) => Some((&i.vis, &i.ident)),
160199
syn::Item::Enum(i) => Some((&i.vis, &i.ident)),
161200
syn::Item::Fn(i) => Some((&i.vis, &i.sig.ident)),
201+
syn::Item::Static(i) => Some((&i.vis, &i.ident)),
162202
syn::Item::Struct(i) => Some((&i.vis, &i.ident)),
163203
syn::Item::Trait(i) => Some((&i.vis, &i.ident)),
204+
syn::Item::TraitAlias(i) => Some((&i.vis, &i.ident)),
164205
syn::Item::Type(i) => Some((&i.vis, &i.ident)),
165206
syn::Item::Union(i) => Some((&i.vis, &i.ident)),
166207
_ => None,
@@ -238,18 +279,20 @@ impl VisitMut for GenFinalPubVisitor {
238279
}
239280

240281
fn visit_item_mod_mut(&mut self, i: &mut syn::ItemMod) {
241-
let old_mod = self.current_mod.clone();
242-
let i_ident = &i.ident;
243-
self.current_mod = self
244-
.current_mod
245-
.as_ref()
246-
.map(|old_mod| parse_quote!(#old_mod::#i_ident));
247-
248-
i.vis = parse_quote!(pub);
282+
// Push
283+
self.current_mod.segments.push(i.ident.clone().into());
284+
self.stack_is_pub
285+
.push(matches!(i.vis, Visibility::Public(_)));
249286

250287
syn::visit_mut::visit_item_mod_mut(self, i);
251288

252-
self.current_mod = old_mod;
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);
253296
}
254297

255298
fn visit_item_fn_mut(&mut self, i: &mut syn::ItemFn) {
@@ -260,7 +303,7 @@ impl VisitMut for GenFinalPubVisitor {
260303
fn visit_item_mut(&mut self, i: &mut syn::Item) {
261304
// TODO(shadaj): warn if a pub struct or enum has private fields
262305
// and is not marked for runtime
263-
let cur_path = self.current_mod.as_ref().unwrap();
306+
let cur_path = &self.current_mod;
264307

265308
// Remove if marked with `#[cfg(stageleft_runtime)]`
266309
if is_runtime(item_attributes(i)) {
@@ -270,6 +313,7 @@ impl VisitMut for GenFinalPubVisitor {
270313

271314
match i {
272315
syn::Item::Macro(m) => {
316+
// TODO(mingwei): Handle if `can_access_current()` is false
273317
if let Some(exported_items) = get_stageleft_export_items(&m.attrs) {
274318
*i = parse_quote! {
275319
pub use #cur_path::{ #( #exported_items ),* };
@@ -284,12 +328,14 @@ impl VisitMut for GenFinalPubVisitor {
284328
// Re-export macro at top-level later.
285329
self.all_macros.push(m.ident.as_ref().unwrap().clone());
286330
*i = syn::Item::Verbatim(Default::default());
331+
return;
287332
}
288333
}
289334
syn::Item::Impl(_e) => {
290-
// TODO(shadaj): emit impls if the struct is private
335+
// TODO(shadaj): emit impls if the **struct** is private
291336
// currently, we just skip all impls
292337
*i = syn::Item::Verbatim(Default::default());
338+
return;
293339
}
294340
syn::Item::Mod(m) => {
295341
let is_test_mod = m
@@ -331,8 +377,10 @@ impl VisitMut for GenFinalPubVisitor {
331377
_ => {}
332378
}
333379

334-
// If a named item has pub visibility, simply re-export from original crate.
335-
if let Some((Visibility::Public(_), name_ident)) = item_visibility_ident(i) {
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((Visibility::Public(_), name_ident)) = item_visibility_ident(i)
383+
{
336384
let cfg_attrs = get_cfg_attrs(item_attributes(i));
337385
*i = parse_quote!(#(#cfg_attrs)* pub use #cur_path::#name_ident;);
338386
return;
@@ -420,18 +468,33 @@ fn gen_deps_module(stageleft_name: syn::Ident, manifest_path: &Path) -> syn::Ite
420468

421469
/// Generates the contents of `mod __staged`, which contains a copy of the crate's code but with
422470
/// all APIs made public so they can be resolved when quoted code is spliced.
471+
///
472+
/// # Arguments
473+
/// * `lib_path` - path to the root Rust file, usually to `lib.rs`.
474+
/// * `orig_crate_path` - Rust module path to the staged crate. Usually `crate`, but may be the staged crate name if
475+
/// the entry and staged crate/target are different.
476+
/// * `is_staged_separate` - Whether the staged crate will be included in a separate crate (instead of the original
477+
/// 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).
478+
/// * `test_mode_feature` - If `Some("FEATURE")`, `#[cfg(test)]` modules will be gated with
479+
/// `#[cfg(feature = "FEATURE")]` instead of being fully removed.
423480
fn gen_staged_mod(
424481
lib_path: &Path,
425-
orig_crate_ident: syn::Path,
482+
orig_crate_path: syn::Path,
426483
test_mode_feature: Option<String>,
484+
is_staged_separate: bool,
427485
) -> syn::File {
486+
assert!(
487+
!orig_crate_path.segments.trailing_punct(),
488+
"`orig_crate_path` may not have trailing `::`"
489+
);
490+
428491
let mut flow_lib_pub = syn_inline_mod::parse_and_inline_modules(lib_path);
429492

430-
let mut final_pub_visitor = GenFinalPubVisitor {
431-
current_mod: Some(parse_quote!(#orig_crate_ident)),
493+
let mut final_pub_visitor = GenFinalPubVisitor::new(
494+
orig_crate_path.clone(),
432495
test_mode_feature,
433-
all_macros: vec![],
434-
};
496+
is_staged_separate,
497+
);
435498
final_pub_visitor.visit_file_mut(&mut flow_lib_pub);
436499

437500
// macros exported with `#[macro_export]` are placed at the top-level of the crate,
@@ -440,7 +503,7 @@ fn gen_staged_mod(
440503
for exported_macro in final_pub_visitor.all_macros {
441504
flow_lib_pub
442505
.items
443-
.push(parse_quote!(pub use #orig_crate_ident::#exported_macro;));
506+
.push(parse_quote!(pub use #orig_crate_path::#exported_macro;));
444507
}
445508

446509
flow_lib_pub
@@ -449,14 +512,23 @@ fn gen_staged_mod(
449512
/// Generates the contents for `__staged` when it will be emitted in "trybuild mode", which means that
450513
/// it is included inline next to the spliced code that uses it, with the original crate available as
451514
/// a dependency.
515+
///
516+
/// # Arguments
517+
/// * `lib_path` - path to the root Rust file, usually to `lib.rs`.
518+
/// * `manifest_path` - path to the package `Cargo.toml`.
519+
/// * `orig_crate_path` - Rust module path to the staged crate. Usually `crate`, but may be the staged crate name if
520+
/// the entry and staged crate/target are different.
521+
/// * `test_mode_feature` - If `Some("FEATURE")`, `#[cfg(test)]` modules will be gated with
522+
/// `#[cfg(feature = "FEATURE")]` instead of being fully removed.
452523
pub fn gen_staged_trybuild(
453524
lib_path: &Path,
454525
manifest_path: &Path,
455-
orig_crate_name: String,
526+
orig_crate_path: &str,
456527
test_mode_feature: Option<String>,
457528
) -> syn::File {
458-
let crate_name = syn::Ident::new(&orig_crate_name, Span::call_site());
459-
let mut flow_lib_pub = gen_staged_mod(lib_path, parse_quote!(#crate_name), test_mode_feature);
529+
let orig_crate_path = syn::parse_str(orig_crate_path)
530+
.expect("Failed to parse `orig_crate_path` as `crate`, crate name, or module path.");
531+
let mut flow_lib_pub = gen_staged_mod(lib_path, orig_crate_path, test_mode_feature, true);
460532

461533
let deps_mod = gen_deps_module(parse_quote!(stageleft), manifest_path);
462534

@@ -484,6 +556,7 @@ pub fn gen_staged_pub() {
484556
.unwrap_or_else(|| Path::new("src/lib.rs")),
485557
parse_quote!(crate),
486558
None,
559+
false,
487560
);
488561

489562
fs::write(

0 commit comments

Comments
 (0)