Skip to main content

miniextendr_macros/
reexport.rs

1//! `reexport!(pkg::name, ...)`: R re-exports declared from Rust (#1910).
2//!
3//! Each item is another package's export that this package exports too. The
4//! macro registers one `ReexportEntry` per item in
5//! `miniextendr_api::registry::MX_REEXPORTS`; the wrapper writer turns them
6//! into roxygen2's re-export blocks (`@importFrom pkg name`, `@export`,
7//! `pkg::name`), one per `pkg::name` however many sites declare it, and
8//! refuses a name re-exported from two packages or bound by the package
9//! itself.
10//!
11//! The package and a syntactic name are dot-joined identifiers
12//! (`data.table::fread`, `stats::na.omit`); a non-syntactic name is a string
13//! literal (`magrittr::"%>%"`). Every problem of one invocation is reported at
14//! once, each on its own tokens.
15
16use proc_macro2::{Span, TokenStream, TokenTree};
17use quote::{ToTokens, quote};
18use syn::ext::IdentExt;
19use syn::parse::{ParseStream, Parser};
20
21/// One `pkg::name` item.
22struct Item {
23    package: String,
24    /// The tokens of the package, for an error on them.
25    package_tokens: TokenStream,
26    name: String,
27    /// The tokens of the name, for an error on them.
28    name_tokens: TokenStream,
29    /// Whether the name is a string literal.
30    literal: bool,
31    /// The item's first token, whose position the entry records.
32    span: Span,
33}
34
35/// Expand `reexport!(...)`.
36pub(crate) fn expand(input: TokenStream) -> TokenStream {
37    match parse_items(input) {
38        Ok(items) => entries(&items),
39        Err(err) => err.into_compile_error(),
40    }
41}
42
43/// The items of an invocation, or every problem found in it.
44fn parse_items(input: TokenStream) -> syn::Result<Vec<Item>> {
45    if input.is_empty() {
46        return Err(syn::Error::new(
47            Span::call_site(),
48            "`reexport!` takes one or more `pkg::name` items, e.g. `reexport!(generics::tidy)`",
49        ));
50    }
51    let mut errors: Option<syn::Error> = None;
52    let mut push = |err: syn::Error| match &mut errors {
53        Some(errors) => errors.combine(err),
54        None => errors = Some(err),
55    };
56    let mut items: Vec<Item> = Vec::new();
57    let segments = split_items(input);
58    let last = segments.len() - 1;
59    for (index, (segment, comma)) in segments.into_iter().enumerate() {
60        if segment.is_empty() {
61            // A trailing comma ends the list; any other empty item is a stray comma.
62            if index != last {
63                push(syn::Error::new(
64                    comma.unwrap_or_else(Span::call_site),
65                    "expected a `pkg::name` item before this `,`",
66                ));
67            }
68            continue;
69        }
70        let item = match parse_item.parse2(segment) {
71            Ok(item) => item,
72            Err(err) => {
73                push(err);
74                continue;
75            }
76        };
77        let mut valid = true;
78        if let Some(problem) = package_problem(&item.package) {
79            push(syn::Error::new_spanned(&item.package_tokens, problem));
80            valid = false;
81        }
82        if let Some(problem) = name_problem(&item.package, &item.name, item.literal) {
83            push(syn::Error::new_spanned(&item.name_tokens, problem));
84            valid = false;
85        }
86        if !valid {
87            continue;
88        }
89        if let Some(earlier) = items
90            .iter()
91            .find(|earlier| earlier.name == item.name && earlier.package != item.package)
92        {
93            push(syn::Error::new_spanned(
94                &item.package_tokens,
95                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}` is already re-exported from `{1}` in this `reexport!`; a package exports one object under a name",
                item.name, earlier.package))
    })format!(
96                    "`{}` is already re-exported from `{}` in this `reexport!`; a package \
97                     exports one object under a name",
98                    item.name, earlier.package
99                ),
100            ));
101            continue;
102        }
103        items.push(item);
104    }
105    match errors {
106        Some(errors) => Err(errors),
107        None => Ok(items),
108    }
109}
110
111/// The input split at its top-level commas: each item's tokens and the comma
112/// after it (`None` after the last). Delimited groups are single token trees,
113/// so a comma inside one does not split.
114fn split_items(input: TokenStream) -> Vec<(TokenStream, Option<Span>)> {
115    let mut segments = Vec::new();
116    let mut current = TokenStream::new();
117    for tree in input {
118        match &tree {
119            TokenTree::Punct(punct) if punct.as_char() == ',' => {
120                segments.push((std::mem::take(&mut current), Some(punct.span())));
121            }
122            _ => current.extend([tree]),
123        }
124    }
125    segments.push((current, None));
126    segments
127}
128
129/// One item: `<dotted identifiers>::<dotted identifiers | string literal>`.
130fn parse_item(input: ParseStream) -> syn::Result<Item> {
131    let span = input.span();
132    let (package, package_tokens) = dotted_identifiers(input, "the package name of `pkg::name`")?;
133    if !input.peek(::syn::token::PathSepsyn::Token![::]) {
134        let message =
135            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected `::` after the package `{0}`: an item is `pkg::name`",
                package))
    })format!("expected `::` after the package `{package}`: an item is `pkg::name`");
136        return Err(if input.is_empty() {
137            syn::Error::new_spanned(&package_tokens, message)
138        } else {
139            input.error(message)
140        });
141    }
142    input.parse::<::syn::token::PathSepsyn::Token![::]>()?;
143    let (name, name_tokens, literal) = if input.peek(syn::LitStr) {
144        let lit: syn::LitStr = input.parse()?;
145        if !lit.suffix().is_empty() {
146            return Err(syn::Error::new_spanned(
147                &lit,
148                "a re-exported name written as a string literal takes no suffix",
149            ));
150        }
151        (lit.value(), lit.to_token_stream(), true)
152    } else {
153        let (name, tokens) = dotted_identifiers(
154            input,
155            "the name of `pkg::name`, as identifiers joined by `.` or a string literal",
156        )?;
157        (name, tokens, false)
158    };
159    if !input.is_empty() {
160        let message = if input.peek(::syn::token::PathSepsyn::Token![::]) {
161            "an item names a package and one of its exports, `pkg::name`; separate items with `,`"
162        } else {
163            "unexpected token after `pkg::name`; separate items with `,`"
164        };
165        return Err(input.error(message));
166    }
167    Ok(Item {
168        package,
169        package_tokens,
170        name,
171        name_tokens,
172        literal,
173        span,
174    })
175}
176
177/// Identifiers joined by `.` (`data.table`, `na.omit`), read as written:
178/// a Rust keyword is an identifier here (`base::as.numeric`), and a raw
179/// identifier loses its `r#`.
180fn dotted_identifiers(input: ParseStream, what: &str) -> syn::Result<(String, TokenStream)> {
181    if !input.peek(syn::Ident::peek_any) {
182        return Err(input.error(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected {0}", what))
    })format!("expected {what}")));
183    }
184    let mut tokens = TokenStream::new();
185    let first = syn::Ident::parse_any(input)?;
186    let mut text = first.unraw().to_string();
187    first.to_tokens(&mut tokens);
188    while input.peek(::syn::token::Dotsyn::Token![.]) && !input.peek(::syn::token::DotDotsyn::Token![..]) {
189        let dot: ::syn::token::Dotsyn::Token![.] = input.parse()?;
190        if !input.peek(syn::Ident::peek_any) {
191            return Err(input.error(
192                "expected an identifier after `.`; write a name that is not identifiers \
193                 joined by `.` as a string literal, `pkg::\"name\"`",
194            ));
195        }
196        let part = syn::Ident::parse_any(input)?;
197        text.push('.');
198        text.push_str(&part.unraw().to_string());
199        dot.to_tokens(&mut tokens);
200        part.to_tokens(&mut tokens);
201    }
202    Ok((text, tokens))
203}
204
205/// Why `package` is not an R package name, if it isn't.
206fn package_problem(package: &str) -> Option<String> {
207    (!crate::naming::is_r_package_name(package)).then(|| {
208        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}` is not an R package name: ASCII letters, digits and `.`, at least two characters, starting with a letter and not ending in `.`",
                package))
    })format!(
209            "`{package}` is not an R package name: ASCII letters, digits and `.`, at least two \
210             characters, starting with a letter and not ending in `.`"
211        )
212    })
213}
214
215/// Why `name` can't be re-exported, if it can't. Written as identifiers it
216/// must be a syntactic R name; a string literal may be any non-empty ASCII
217/// text without whitespace, control characters or the quoting characters
218/// `` ` ``, `\` and `"`, which the generated R code and roxygen2 tags write
219/// verbatim or between quotes.
220fn name_problem(package: &str, name: &str, literal: bool) -> Option<String> {
221    if !literal {
222        return (!crate::naming::is_syntactic_r_name(name)).then(|| {
223            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}` is not a syntactic R name; write a non-syntactic name as a string literal, `{1}::\"{0}\"`",
                name, package))
    })format!(
224                "`{name}` is not a syntactic R name; write a non-syntactic name as a string \
225                 literal, `{package}::\"{name}\"`"
226            )
227        });
228    }
229    if name.is_empty() {
230        Some("a re-exported name can't be empty".to_string())
231    } else if !name.is_ascii() {
232        Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}` is not ASCII; R code in a package must be ASCII",
                name))
    })format!(
233            "`{name}` is not ASCII; R code in a package must be ASCII"
234        ))
235    } else if name
236        .chars()
237        .any(|c| c.is_ascii_whitespace() || c.is_ascii_control() || #[allow(non_exhaustive_omitted_patterns)] match c {
    '`' | '\\' | '"' => true,
    _ => false,
}matches!(c, '`' | '\\' | '"'))
238    {
239        Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("the re-exported name `{0}` can\'t hold whitespace, control characters, `` ` ``, `\\` or `\"`",
                name))
    })format!(
240            "the re-exported name `{name}` can't hold whitespace, control characters, `` ` ``, \
241             `\\` or `\"`"
242        ))
243    } else {
244        None
245    }
246}
247
248/// One `MX_REEXPORTS` entry per item, in an anonymous `const _` so any number
249/// of invocations live side by side. The slice is host-only, so on wasm32 the
250/// invocation expands to nothing.
251fn entries(items: &[Item]) -> TokenStream {
252    let statics = items.iter().enumerate().map(|(index, item)| {
253        let ident = ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("REEXPORT_{0}", index))
            }), ::quote::__private::Option::None)quote::format_ident!("REEXPORT_{index}");
254        let package = &item.package;
255        let name = &item.name;
256        let start = item.span.start();
257        let source_line = syn::LitInt::new(&start.line.to_string(), item.span);
258        let source_column = syn::LitInt::new(&(start.column + 1).to_string(), item.span);
259        {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::__private::push_pound(&mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Bracket,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "miniextendr_api");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "linkme");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "distributed_slice");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "registry");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "MX_REEXPORTS");
                    _s
                });
            _s
        });
    ::quote::__private::push_pound(&mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Bracket,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "linkme");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "crate");
                    ::quote::__private::push_eq(&mut _s);
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "linkme");
                    _s
                });
            _s
        });
    ::quote::__private::push_ident(&mut _s, "static");
    ::quote::ToTokens::to_tokens(&ident, &mut _s);
    ::quote::__private::push_colon(&mut _s);
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "registry");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "ReexportEntry");
    ::quote::__private::push_eq(&mut _s);
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "registry");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "ReexportEntry");
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Brace,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "package");
            ::quote::__private::push_colon(&mut _s);
            ::quote::ToTokens::to_tokens(&package, &mut _s);
            ::quote::__private::push_comma(&mut _s);
            ::quote::__private::push_ident(&mut _s, "name");
            ::quote::__private::push_colon(&mut _s);
            ::quote::ToTokens::to_tokens(&name, &mut _s);
            ::quote::__private::push_comma(&mut _s);
            ::quote::__private::push_ident(&mut _s, "source_file");
            ::quote::__private::push_colon(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "file");
            ::quote::__private::push_bang(&mut _s);
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                ::quote::__private::TokenStream::new());
            ::quote::__private::push_comma(&mut _s);
            ::quote::__private::push_ident(&mut _s, "source_line");
            ::quote::__private::push_colon(&mut _s);
            ::quote::ToTokens::to_tokens(&source_line, &mut _s);
            ::quote::__private::push_comma(&mut _s);
            ::quote::__private::push_ident(&mut _s, "source_column");
            ::quote::__private::push_colon(&mut _s);
            ::quote::ToTokens::to_tokens(&source_column, &mut _s);
            ::quote::__private::push_comma(&mut _s);
            _s
        });
    ::quote::__private::push_semi(&mut _s);
    _s
}quote! {
260            #[::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_REEXPORTS)]
261            #[linkme(crate = ::miniextendr_api::linkme)]
262            static #ident: ::miniextendr_api::registry::ReexportEntry =
263                ::miniextendr_api::registry::ReexportEntry {
264                    package: #package,
265                    name: #name,
266                    source_file: ::core::file!(),
267                    source_line: #source_line,
268                    source_column: #source_column,
269                };
270        }
271    });
272    {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::__private::push_pound(&mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Bracket,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "cfg");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "not");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Parenthesis,
                        {
                            let mut _s = ::quote::__private::TokenStream::new();
                            ::quote::__private::push_ident(&mut _s, "target_arch");
                            ::quote::__private::push_eq(&mut _s);
                            ::quote::__private::parse(&mut _s, "\"wasm32\"");
                            _s
                        });
                    _s
                });
            _s
        });
    ::quote::__private::push_ident(&mut _s, "const");
    ::quote::__private::push_underscore(&mut _s);
    ::quote::__private::push_colon(&mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Parenthesis,
        ::quote::__private::TokenStream::new());
    ::quote::__private::push_eq(&mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Brace,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            {
                use ::quote::__private::ext::*;
                let has_iter = ::quote::__private::HasIterator::<false>;
                #[allow(unused_mut)]
                let (mut statics, i) = statics.quote_into_iter();
                let has_iter = has_iter | i;
                <_ as
                        ::quote::__private::CheckHasIterator<true>>::check(has_iter);
                while true {
                    let statics =
                        match statics.next() {
                            Some(_x) => ::quote::__private::RepInterp(_x),
                            None => break,
                        };
                    ::quote::ToTokens::to_tokens(&statics, &mut _s);
                }
            }
            _s
        });
    ::quote::__private::push_semi(&mut _s);
    _s
}quote! {
273        #[cfg(not(target_arch = "wasm32"))]
274        const _: () = {
275            #(#statics)*
276        };
277    }
278}
279
280#[cfg(test)]
281mod tests;