layered_crate/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use proc_macro::TokenStream;
4use proc_macro2::Span as Span2;
5use proc_macro2::TokenStream as TokenStream2;
6use quote::quote;
7use quote::quote_spanned;
8use syn::parse_macro_input;
9
10/// See [`crate documentation`](crate)
11#[proc_macro_attribute]
12pub fn layers(_attr: TokenStream, input: TokenStream) -> TokenStream {
13    let input = parse_macro_input!(input as syn::ItemMod);
14    match layered_crate_expand(input) {
15        Ok(expanded) => expanded,
16        Err(err) => err.to_compile_error().into(),
17    }
18}
19
20fn layered_crate_expand(input: syn::ItemMod) -> syn::Result<TokenStream> {
21    let (_, content) = match input.content {
22        None => {
23            // nothing in the mod
24            return Ok(quote! { #input }.into());
25        }
26        Some(content) => content,
27    };
28
29    let mut before_tokens = TokenStream2::new();
30    let mut has_doc_hidden = false;
31
32    // keep the original attributes on the whole import
33    // and ensure #[doc(hidden)] is present
34    for attr in input.attrs {
35        if attr.path().is_ident("doc") {
36            if let Ok(x) = attr
37                .meta
38                .require_list()
39                .and_then(|m| m.parse_args::<syn::Ident>())
40            {
41                if x == "hidden" {
42                    has_doc_hidden = true;
43                }
44            }
45        }
46        before_tokens.extend(quote! { #attr });
47    }
48    if !has_doc_hidden {
49        before_tokens.extend(quote! { #[doc(hidden)] });
50    }
51
52    // collect the dependency attributes
53    let mut graph = graph::DepsGraph::default();
54    let mut transformed_src_content = TokenStream2::new();
55    let mut error_tokens = TokenStream2::new();
56
57    for item in content {
58        // attrs  vis              ident  extra_tokens
59        // #[...] pub mod          xxx    {...}
60        // #[...] pub mod          yyy    ;
61        // #[...] pub extern crate zzz    ;
62        let (attrs, vis, ident, extra_tokens) = match item {
63            // Limitation - non-inline modules in proc-macro is unstable
64            // so as a workaround we use "extern crate" as a placeholder
65            // for non-inline modules
66            syn::Item::ExternCrate(item) => {
67                let mut extra_tokens = TokenStream2::new();
68                if let Some(rename) = item.rename {
69                    let e = syn::Error::new_spanned(
70                        &rename.1,
71                        "rename syntax (as ...) is not supported when using #[layers]",
72                    );
73                    extra_tokens.extend(e.to_compile_error());
74                }
75                let semi = item.semi_token;
76                extra_tokens.extend(quote! { #semi });
77
78                (item.attrs, item.vis, item.ident, extra_tokens)
79            }
80            syn::Item::Mod(item) => {
81                let mut extra_tokens = TokenStream2::new();
82                if let Some((_, content)) = item.content {
83                    extra_tokens.extend(quote! { { #(#content)* } });
84                }
85                if let Some(semi) = item.semi {
86                    extra_tokens.extend(quote! { #semi });
87                }
88
89                (item.attrs, item.vis, item.ident, extra_tokens)
90            }
91            _ => {
92                // other items in the mod, we just leave them along
93                transformed_src_content.extend(quote! { #item });
94                continue;
95            }
96        };
97
98        // Extract the attributes
99        let mut edges = Vec::with_capacity(attrs.len());
100        let mut docs = TokenStream2::new();
101        let mut cfg = TokenStream2::new();
102        for attr in attrs {
103            if attr.path().is_ident("depends_on") {
104                let ident = match attr
105                    .meta
106                    .require_list()
107                    .and_then(|m| m.parse_args::<syn::Ident>())
108                {
109                    Ok(x) => x,
110                    Err(e) => {
111                        error_tokens.extend(e.to_compile_error());
112                        continue;
113                    }
114                };
115                edges.push(graph::DepEdge {
116                    name: ident.to_string(),
117                    attr,
118                    ident,
119                });
120                continue;
121            }
122
123            if attr.path().is_ident("doc") {
124                docs.extend(quote! { #attr });
125            }
126            if attr.path().is_ident("cfg") {
127                cfg.extend(quote! { #attr });
128            }
129
130            // keep attributes unrelated to us
131            transformed_src_content.extend(quote! { #attr });
132        }
133
134        transformed_src_content.extend(quote! {
135            pub mod #ident #extra_tokens
136        });
137        graph.add(graph::ModuleDecl::new(
138            matches!(vis, syn::Visibility::Public(_)),
139            ident,
140            docs,
141            cfg,
142            edges,
143        ));
144    }
145
146    // check - this produces the errors as tokens instead of
147    // result. we still emit the expanded output even if check fails,
148    // so that we don't cause massive compile failures
149    error_tokens.extend(graph.check());
150
151    // create a new ident, so unused warnings don't show up
152    // on the entire macro input
153    let src_ident = syn::Ident::new(&input.ident.to_string(), Span2::call_site());
154    let mod_tokens = graph.generate_impl(&src_ident);
155
156    let expanded = quote! {
157        #before_tokens
158        pub(crate) mod #src_ident {
159            #transformed_src_content
160        }
161        #mod_tokens
162        #error_tokens
163    };
164
165    Ok(expanded.into())
166}
167
168mod graph;
169
170impl graph::DepsGraph {
171    fn generate_impl(&self, src_mod: &syn::Ident) -> TokenStream2 {
172        let mut mod_tokens = TokenStream2::new();
173        for entry in self.graph.values() {
174            mod_tokens.extend(self.generate_mod_impl(entry, src_mod, self.has_circular_deps));
175        }
176        mod_tokens
177    }
178    fn generate_mod_impl(
179        &self,
180        module: &graph::ModuleDecl,
181        src_mod: &syn::Ident,
182        has_circular_deps: bool,
183    ) -> TokenStream2 {
184        let vis = if module.is_pub {
185            quote! { pub }
186        } else {
187            quote! { pub(crate) }
188        };
189        let doc = &module.docs;
190        let cfg = &module.cfg;
191        let deps_ident = &module.ident;
192
193        if module.edges.is_empty() {
194            return quote_spanned! {
195                module.ident.span() =>
196                    #cfg
197                    #[doc(inline)]
198                    #vis use #src_mod::#deps_ident;
199            };
200        }
201
202        let mut suppress_lints = TokenStream2::new();
203        if has_circular_deps {
204            // allow unused imports in circular deps, because
205            // the warning will make it hard to see what actually is the cause
206            suppress_lints.extend(quote! {
207                #[allow(unused_imports)]
208            });
209        }
210
211        let mut dep_tokens = TokenStream2::new();
212        for edge in &module.edges {
213            let dep_module = self.graph.get(&edge.name).unwrap();
214            let dep_cfg = &dep_module.cfg;
215            let dep_ident = &edge.ident;
216            dep_tokens.extend(quote_spanned! {
217                dep_ident.span() =>
218                    #dep_cfg
219                    pub use crate::#src_mod::#dep_ident;
220            });
221        }
222
223        quote_spanned! {
224            module.ident.span() =>
225                #doc
226                #cfg
227                #vis mod #deps_ident {
228                    #[doc(inline)]
229                    pub use crate::#src_mod::#deps_ident::*;
230                    #[doc(hidden)]
231                    #suppress_lints
232                    pub(crate) mod crate_ {
233                        #dep_tokens
234                    }
235                }
236        }
237    }
238}