Skip to main content

split_modules/
classify.rs

1//! Classify each top-level item: keep it in the parent, or move it to a child
2//! module file (with the visibility rewrites + re-export needed to preserve the API).
3//!
4//! ## Why we widen visibility
5//!
6//! Moving an item from module `M` into `M::child` changes which modules are
7//! descendants of the item's defining module. A *private* field/method is visible to
8//! `M` and all of `M`'s descendants; after the move it is only visible to
9//! `M::child`'s subtree, so sibling modules that relied on the old nesting break.
10//!
11//! Widening such members to `pub(crate)` is a **superset** of any in-crate audience
12//! they previously had, so it can never break code that used to compile, and it does
13//! not change the external API (the item's *name* is re-exported at its original
14//! visibility, and `pub(crate)` members remain invisible outside the crate).
15
16use crate::util::to_snake;
17use proc_macro2::Span;
18use quote::quote;
19use syn::spanned::Spanned;
20use syn::{Attribute, Field, ImplItem, Item, Type, Visibility};
21
22/// Result of classifying one item.
23pub enum ItemClass {
24    /// Stays in the parent module verbatim (`use`, `mod`, `macro_rules!`, …).
25    Keep,
26    /// Moves into a child file.
27    Move(MoveInfo),
28}
29
30pub struct MoveInfo {
31    /// Target file stem.
32    pub group: String,
33    /// Re-export for the parent (vis string, ident, cfg attrs). `None` for `impl`.
34    pub reexport: Option<(String, String, Vec<String>)>,
35    /// Visibility rewrites (item + members), in *absolute* source byte coordinates.
36    pub vis_edits_abs: Vec<AbsVisEdit>,
37}
38
39/// A visibility edit in absolute source coordinates. A zero-width range is an insertion.
40pub struct AbsVisEdit {
41    pub start: usize,
42    pub end: usize,
43    pub text: String,
44}
45
46/// Render a visibility exactly as written (empty string for inherited/private).
47fn render_vis(vis: &Visibility) -> String {
48    match vis {
49        Visibility::Inherited => String::new(),
50        v => quote!(#v).to_string(),
51    }
52}
53
54/// Extract `#[cfg(...)]` / `#[cfg_attr(...)]` attributes as rendered strings.
55fn cfg_attrs(attrs: &[Attribute]) -> Vec<String> {
56    attrs
57        .iter()
58        .filter(|a| a.path().is_ident("cfg") || a.path().is_ident("cfg_attr"))
59        .map(|a| quote!(#a).to_string())
60        .collect()
61}
62
63fn tok_start(span: Span) -> usize {
64    span.byte_range().start
65}
66
67/// Produce an edit that widens `vis` to at least `pub(crate)`, inserting at
68/// `insert_at` when the visibility is currently inherited (private).
69///
70/// Returns `None` when no change is needed (already `pub` or `pub(crate)`).
71fn widen_to_crate(vis: &Visibility, insert_at: usize) -> Option<AbsVisEdit> {
72    match vis {
73        Visibility::Public(_) => None,
74        Visibility::Restricted(r) => {
75            if r.in_token.is_none() && r.path.is_ident("crate") {
76                None
77            } else {
78                let span = vis.span().byte_range();
79                Some(AbsVisEdit { start: span.start, end: span.end, text: "pub(crate)".into() })
80            }
81        }
82        Visibility::Inherited => Some(AbsVisEdit {
83            start: insert_at,
84            end: insert_at,
85            text: "pub(crate) ".into(),
86        }),
87    }
88}
89
90/// Base identifier of a (possibly wrapped) type, used to key `impl` blocks.
91fn type_base_ident(ty: &Type) -> Option<String> {
92    match ty {
93        Type::Path(p) => p.path.segments.last().map(|s| s.ident.to_string()),
94        Type::Reference(r) => type_base_ident(&r.elem),
95        Type::Paren(p) => type_base_ident(&p.elem),
96        Type::Group(g) => type_base_ident(&g.elem),
97        Type::Slice(s) => type_base_ident(&s.elem),
98        Type::Array(a) => type_base_ident(&a.elem),
99        Type::Ptr(p) => type_base_ident(&p.elem),
100        _ => None,
101    }
102}
103
104fn min_start(opts: &[Option<Span>], keyword: Span) -> usize {
105    let mut m = tok_start(keyword);
106    for o in opts.iter().flatten() {
107        m = m.min(tok_start(*o));
108    }
109    m
110}
111
112/// Insertion offset for a field's visibility (after attributes, before the name/type).
113fn field_insert_at(field: &Field) -> usize {
114    match &field.ident {
115        Some(id) => tok_start(id.span()),
116        None => tok_start(field.ty.span()),
117    }
118}
119
120/// Collect widening edits for the members of a struct/union/inherent-impl.
121fn member_edits(item: &Item) -> Vec<AbsVisEdit> {
122    let mut edits = Vec::new();
123    match item {
124        Item::Struct(it) => {
125            for f in &it.fields {
126                if let Some(e) = widen_to_crate(&f.vis, field_insert_at(f)) {
127                    edits.push(e);
128                }
129            }
130        }
131        Item::Union(it) => {
132            for f in &it.fields.named {
133                if let Some(e) = widen_to_crate(&f.vis, field_insert_at(f)) {
134                    edits.push(e);
135                }
136            }
137        }
138        Item::Impl(it) if it.trait_.is_none() => {
139            // Inherent impl: members carry their own visibility. (Trait impls do not,
140            // and adding `pub` there is a hard error — so we skip them.)
141            for member in &it.items {
142                match member {
143                    ImplItem::Fn(f) => {
144                        let insert = min_start(
145                            &[
146                                f.sig.constness.map(|t| t.span()),
147                                f.sig.asyncness.map(|t| t.span()),
148                                f.sig.unsafety.map(|t| t.span()),
149                                f.sig.abi.as_ref().map(|a| a.extern_token.span()),
150                            ],
151                            f.sig.fn_token.span(),
152                        );
153                        if let Some(e) = widen_to_crate(&f.vis, insert) {
154                            edits.push(e);
155                        }
156                    }
157                    ImplItem::Const(c) => {
158                        if let Some(e) = widen_to_crate(&c.vis, tok_start(c.const_token.span())) {
159                            edits.push(e);
160                        }
161                    }
162                    ImplItem::Type(t) => {
163                        if let Some(e) = widen_to_crate(&t.vis, tok_start(t.type_token.span())) {
164                            edits.push(e);
165                        }
166                    }
167                    _ => {}
168                }
169            }
170        }
171        _ => {}
172    }
173    edits
174}
175
176pub fn classify(item: &Item) -> ItemClass {
177    // Item + member visibility widening, plus module-relative path rewrites.
178    let mut members = member_edits(item);
179    members.extend(crate::pathfix::relative_path_edits(item));
180    match item {
181        Item::Struct(it) => named(&it.vis, &it.ident, tok_start(it.struct_token.span()), &it.attrs, members),
182        Item::Enum(it) => named(&it.vis, &it.ident, tok_start(it.enum_token.span()), &it.attrs, members),
183        Item::Union(it) => named(&it.vis, &it.ident, tok_start(it.union_token.span()), &it.attrs, members),
184        Item::Trait(it) => {
185            let insert = min_start(
186                &[it.unsafety.map(|t| t.span()), it.auto_token.map(|t| t.span())],
187                it.trait_token.span(),
188            );
189            named(&it.vis, &it.ident, insert, &it.attrs, members)
190        }
191        Item::TraitAlias(it) => named(&it.vis, &it.ident, tok_start(it.trait_token.span()), &it.attrs, members),
192        Item::Type(it) => named(&it.vis, &it.ident, tok_start(it.type_token.span()), &it.attrs, members),
193        Item::Const(it) => named(&it.vis, &it.ident, tok_start(it.const_token.span()), &it.attrs, members),
194        Item::Static(it) => named(&it.vis, &it.ident, tok_start(it.static_token.span()), &it.attrs, members),
195        Item::Fn(it) => {
196            let insert = min_start(
197                &[
198                    it.sig.constness.map(|t| t.span()),
199                    it.sig.asyncness.map(|t| t.span()),
200                    it.sig.unsafety.map(|t| t.span()),
201                    it.sig.abi.as_ref().map(|a| a.extern_token.span()),
202                ],
203                it.sig.fn_token.span(),
204            );
205            named(&it.vis, &it.sig.ident, insert, &it.attrs, members)
206        }
207        Item::Impl(it) => {
208            let group = type_base_ident(&it.self_ty)
209                .map(|s| to_snake(&s))
210                .unwrap_or_else(|| "impls".to_string());
211            ItemClass::Move(MoveInfo { group, reexport: None, vis_edits_abs: members })
212        }
213        _ => ItemClass::Keep,
214    }
215}
216
217fn named(
218    vis: &Visibility,
219    ident: &syn::Ident,
220    insert_at: usize,
221    attrs: &[Attribute],
222    mut edits: Vec<AbsVisEdit>,
223) -> ItemClass {
224    let name = ident.to_string();
225    // Leave these in the parent untouched:
226    //  * anonymous items (`const _: T = ...;`) — can't be re-exported by name;
227    //  * `_`-prefixed items — conventionally side-effect-only (e.g. compile-time
228    //    assertions), so re-exporting them just yields dead `use` warnings;
229    //  * raw identifiers — awkward to re-export and rare.
230    // Kept private items stay reachable from child modules via `use super::*`.
231    if name.starts_with('_') || name.starts_with("r#") {
232        return ItemClass::Keep;
233    }
234    let group = to_snake(&name);
235    let reexport = Some((render_vis(vis), name, cfg_attrs(attrs)));
236    if let Some(e) = widen_to_crate(vis, insert_at) {
237        edits.push(e);
238    }
239    ItemClass::Move(MoveInfo { group, reexport, vis_edits_abs: edits })
240}