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.
1516use proc_macro2::{Span, TokenStream, TokenTree};
17use quote::{ToTokens, quote};
18use syn::ext::IdentExt;
19use syn::parse::{ParseStream, Parser};
2021/// One `pkg::name` item.
22struct Item {
23 package: String,
24/// The tokens of the package, for an error on them.
25package_tokens: TokenStream,
26 name: String,
27/// The tokens of the name, for an error on them.
28name_tokens: TokenStream,
29/// Whether the name is a string literal.
30literal: bool,
31/// The item's first token, whose position the entry records.
32span: Span,
33}
3435/// Expand `reexport!(...)`.
36pub(crate) fn expand(input: TokenStream) -> TokenStream {
37match parse_items(input) {
38Ok(items) => entries(&items),
39Err(err) => err.into_compile_error(),
40 }
41}
4243/// The items of an invocation, or every problem found in it.
44fn parse_items(input: TokenStream) -> syn::Result<Vec<Item>> {
45if input.is_empty() {
46return Err(syn::Error::new(
47Span::call_site(),
48"`reexport!` takes one or more `pkg::name` items, e.g. `reexport!(generics::tidy)`",
49 ));
50 }
51let mut errors: Option<syn::Error> = None;
52let mut push = |err: syn::Error| match &mut errors {
53Some(errors) => errors.combine(err),
54None => errors = Some(err),
55 };
56let mut items: Vec<Item> = Vec::new();
57let segments = split_items(input);
58let last = segments.len() - 1;
59for (index, (segment, comma)) in segments.into_iter().enumerate() {
60if segment.is_empty() {
61// A trailing comma ends the list; any other empty item is a stray comma.
62if 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 }
68continue;
69 }
70let item = match parse_item.parse2(segment) {
71Ok(item) => item,
72Err(err) => {
73 push(err);
74continue;
75 }
76 };
77let mut valid = true;
78if let Some(problem) = package_problem(&item.package) {
79 push(syn::Error::new_spanned(&item.package_tokens, problem));
80 valid = false;
81 }
82if 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 }
86if !valid {
87continue;
88 }
89if 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 ));
101continue;
102 }
103 items.push(item);
104 }
105match errors {
106Some(errors) => Err(errors),
107None => Ok(items),
108 }
109}
110111/// 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>)> {
115let mut segments = Vec::new();
116let mut current = TokenStream::new();
117for tree in input {
118match &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 }
125segments.push((current, None));
126segments127}
128129/// One item: `<dotted identifiers>::<dotted identifiers | string literal>`.
130fn parse_item(input: ParseStream) -> syn::Result<Item> {
131let span = input.span();
132let (package, package_tokens) = dotted_identifiers(input, "the package name of `pkg::name`")?;
133if !input.peek(::syn::token::PathSepsyn::Token![::]) {
134let 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`");
136return Err(if input.is_empty() {
137 syn::Error::new_spanned(&package_tokens, message)
138 } else {
139input.error(message)
140 });
141 }
142 input.parse::<::syn::token::PathSepsyn::Token![::]>()?;
143let (name, name_tokens, literal) = if input.peek(syn::LitStr) {
144let lit: syn::LitStr = input.parse()?;
145if !lit.suffix().is_empty() {
146return 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 {
153let (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 };
159if !input.is_empty() {
160let 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};
165return Err(input.error(message));
166 }
167Ok(Item {
168package,
169package_tokens,
170name,
171name_tokens,
172literal,
173span,
174 })
175}
176177/// 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)> {
181if !input.peek(syn::Ident::peek_any) {
182return Err(input.error(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("expected {0}", what))
})format!("expected {what}")));
183 }
184let mut tokens = TokenStream::new();
185let first = syn::Ident::parse_any(input)?;
186let mut text = first.unraw().to_string();
187first.to_tokens(&mut tokens);
188while input.peek(::syn::token::Dotsyn::Token![.]) && !input.peek(::syn::token::DotDotsyn::Token![..]) {
189let dot: ::syn::token::Dotsyn::Token![.] = input.parse()?;
190if !input.peek(syn::Ident::peek_any) {
191return 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 }
196let 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 }
202Ok((text, tokens))
203}
204205/// 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}
214215/// 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> {
221if !literal {
222return (!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 }
229if name.is_empty() {
230Some("a re-exported name can't be empty".to_string())
231 } else if !name.is_ascii() {
232Some(::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 name236 .chars()
237 .any(|c| c.is_ascii_whitespace() || c.is_ascii_control() || #[allow(non_exhaustive_omitted_patterns)] match c {
'`' | '\\' | '"' => true,
_ => false,
}matches!(c, '`' | '\\' | '"'))
238 {
239Some(::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 {
244None245 }
246}
247248/// 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 {
252let statics = items.iter().enumerate().map(|(index, item)| {
253let 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}");
254let package = &item.package;
255let name = &item.name;
256let start = item.span.start();
257let source_line = syn::LitInt::new(&start.line.to_string(), item.span);
258let 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)]
262static #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"))]
274const _: () = {
275 #(#statics)*
276 };
277 }278}
279280#[cfg(test)]
281mod tests;