attr_alias/lib.rs
1//! This crate allows defining arbitrary aliases for attributes.
2//!
3//! Aliases are resolved by [`#[attr_alias]`][macro@attr_alias]. Since that
4//! attribute requires a nightly compiler, [`#[eval]`][macro@eval] and
5//! [`eval_block!`] provide workarounds for use on the stable release channel.
6//!
7//! # Alias File
8//!
9//! Due to how procedural macros work and to avoid redundancy, this crate will
10//! always read aliases from
11#![doc = concat!("\"", alias_file!(), "\".")]
12//! Other files may be supported in future versions, but doing so is not
13//! currently possible. Open an issue if this is important for your build.
14//!
15//! ## Syntax
16//!
17//! - Each alias must begin with `*` and be assigned a valid attribute value.
18//! - Aliases can reference others, but referenced aliases must be listed
19//! first.
20//!
21//! ## Example
22//!
23//! ```ignore
24#![doc = include_str!(concat!("../", alias_file!()))]
25//! ```
26//!
27//! # Features
28//!
29//! These features are optional and can be enabled or disabled in a
30//! "Cargo.toml" file.
31//!
32//! ### Nightly Features
33//!
34//! These features are unstable, since they rely on unstable Rust features.
35//!
36//! - **nightly** -
37//! Provides [`#[attr_alias]`][macro@attr_alias].
38//!
39//! # Dependencies
40//!
41//! Although this is a proc\_macro crate, it does not depend on [proc\_macro2],
42//! [quote], or [syn]. Therefore, its impact on compile time should be minimal.
43//!
44//! # Comparable Crates
45//!
46//! The following crates are similar but take different approaches. An overview
47//! of benefits and downsides in comparison to this crate is provided for each
48//! when expanded.
49//!
50//! <ul><li><details><summary>
51//!
52//! **[cfg\_aliases]** -
53//! Aliases defined using "build.rs" instructions.
54//!
55//! </summary>
56//!
57//! - *Pros:*
58//! - Compile time may be reduced. The declarative macro is only used in
59//! the build file, but the build file must be compiled as well.
60//! - Inner attributes are supported without a nightly feature.
61//! - *Cons:*
62//! - Only `#[cfg]` aliases can be defined.
63//! - Some configuration options are not supported (e.g., `test`).
64//! - Alias names are not checked at compile time.
65//! - Aliases are not expanded inline, as would be desirable for
66//! `#[doc(cfg)]`.
67//!
68//! </details></li><li><details><summary>
69//!
70//! **[macro\_rules\_attribute]** -
71//! Aliases defined as declarative macros.
72//!
73//! </summary>
74//!
75//! - *Pros:*
76//! - Aliases are defined within Rust source files.
77//! - Aliases can expand to multiple attributes.
78//! - Declarative macros accepting valid Rust syntax can be used as
79//! attributes.
80//! - *Cons:*
81//! - Attributes cannot be attached to statements without a nightly
82//! feature.
83//! - Inner attributes are not supported.
84//! - Aliases cannot be inserted at a specific part of an attribute
85//! (e.g., within `not()`).
86//! - Some dependencies are required, which may impact compile time.
87//!
88//! </details></li></ul>
89//!
90//! [cfg\_aliases]: https://crates.io/crates/cfg_aliases
91//! [macro\_rules\_attribute]: https://crates.io/crates/macro_rules_attribute
92//! [proc\_macro2]: https://crates.io/crates/proc_macro2
93//! [quote]: https://crates.io/crates/quote
94//! [syn]: https://crates.io/crates/syn
95
96// Only require a nightly compiler when building documentation for docs.rs.
97// This is a private option that should not be used.
98// https://github.com/rust-lang/docs.rs/issues/147#issuecomment-389544407
99#![cfg_attr(feature = "nightly", feature(doc_cfg))]
100#![cfg_attr(feature = "nightly", feature(track_path))]
101#![forbid(unsafe_code)]
102#![warn(unused_results)]
103
104use std::error;
105use std::result;
106
107#[cfg(feature = "nightly")]
108use proc_macro::tracked_path;
109use proc_macro::Delimiter;
110use proc_macro::Group;
111use proc_macro::Literal;
112use proc_macro::Punct;
113use proc_macro::Spacing;
114use proc_macro::Span;
115use proc_macro::TokenStream;
116use proc_macro::TokenTree;
117
118macro_rules! alias_file {
119 () => {
120 "src/attr-aliases.txt"
121 };
122}
123use alias_file;
124
125macro_rules! tokens {
126 ( $($token:expr ,)+ ) => {{
127 use proc_macro::TokenTree;
128
129 [$(TokenTree::from($token)),+].into_iter()
130 }};
131}
132
133macro_rules! path {
134 ( $($name:expr),+ ) => {{
135 use proc_macro::Ident;
136 use proc_macro::Punct;
137 use proc_macro::Spacing;
138 use proc_macro::Span;
139
140 tokens!(
141 $(
142 Punct::new(':', Spacing::Joint),
143 Punct::new(':', Spacing::Alone),
144 Ident::new($name, Span::call_site()),
145 )+
146 )
147 }};
148}
149
150mod aliases;
151use aliases::Aliases;
152
153fn core_macro(name: &str, arg: &str) -> impl Iterator<Item = TokenTree> {
154 path!("core", name).chain(tokens!(
155 Punct::new('!', Spacing::Alone),
156 Group::new(
157 Delimiter::Parenthesis,
158 TokenTree::Literal(Literal::string(arg)).into(),
159 ),
160 Punct::new(';', Spacing::Alone),
161 ))
162}
163
164struct Error {
165 span: Span,
166 message: String,
167}
168
169impl Error {
170 fn new(message: &'static str) -> Self {
171 Self {
172 span: Span::call_site(),
173 message: message.to_owned(),
174 }
175 }
176
177 fn new_from<T>(error: T, message: &'static str) -> Self
178 where
179 T: error::Error,
180 {
181 Self {
182 span: Span::call_site(),
183 message: format!("error {}: {}", message, error),
184 }
185 }
186
187 fn token(token: &TokenTree) -> Self {
188 Self {
189 span: token.span(),
190 message: "unexpected token".to_owned(),
191 }
192 }
193
194 fn into_compile_error(self) -> TokenStream {
195 core_macro("compile_error", &self.message)
196 .map(|mut token| {
197 token.set_span(self.span);
198 token
199 })
200 .collect()
201 }
202}
203
204fn parse_empty<I>(tokens: I) -> Result<()>
205where
206 I: IntoIterator<Item = TokenTree>,
207{
208 tokens
209 .into_iter()
210 .next()
211 .map(|x| Err(Error::token(&x)))
212 .unwrap_or(Ok(()))
213}
214
215type Result<T> = result::Result<T, Error>;
216
217fn eval_item(item: TokenStream, resolved: &mut bool) -> Result<TokenStream> {
218 let mut attr = false;
219 item.into_iter()
220 .map(|mut token| {
221 if let TokenTree::Group(group) = &mut token {
222 let delimiter = group.delimiter();
223 let mut stream = group.stream();
224 if attr && delimiter == Delimiter::Bracket {
225 *resolved |= Aliases::get()?.resolve(&mut stream)?;
226 } else {
227 stream = eval_item(stream, resolved)?;
228 }
229 *group = Group::new(delimiter, stream);
230 }
231 attr = matches!(
232 &token,
233 TokenTree::Punct(x)
234 if x.as_char() == '#' || (attr && x.as_char() == '!'),
235 );
236 Ok(token)
237 })
238 .collect()
239}
240
241/// Resolves an alias using a pattern.
242///
243/// # Arguments
244///
245/// The following positional arguments are expected:
246/// 1. *alias name* - required and must be a valid [Rust identifier]
247/// 2. *expansion pattern* - optional and may include `*` wildcards
248/// - The first wildcard in this pattern will be replaced with the expanded
249/// alias.
250/// - If not specified, this argument defaults to the value of the
251/// "default" alias, or `*` if that alias is not defined.
252///
253/// For example, using the [example alias file], the annotations
254/// `#[attr_alias(macos, cfg(*))]` and `#[attr_alias(macos)]` would both expand
255/// to `#[cfg(target_os = "macos")]`.
256///
257/// # Examples
258///
259/// *Compiled using the [example alias file].*
260///
261/// ```
262/// # #![feature(doc_cfg)]
263/// #
264/// use std::process::Command;
265///
266/// use attr_alias::attr_alias;
267///
268/// struct ProcessBuilder(Command);
269///
270/// impl ProcessBuilder {
271/// #[attr_alias(macos_or_windows)]
272/// #[attr_alias(macos_or_windows, doc(cfg(*)))]
273/// fn name(&mut self, name: &str) -> &mut Self {
274/// unimplemented!();
275/// }
276/// }
277/// ```
278///
279/// [example alias file]: self#example
280/// [Rust identifier]: https://doc.rust-lang.org/reference/identifiers.html
281#[cfg(feature = "nightly")]
282#[cfg_attr(feature = "nightly", doc(cfg(feature = "nightly")))]
283#[proc_macro_attribute]
284pub fn attr_alias(args: TokenStream, item: TokenStream) -> TokenStream {
285 tracked_path::path(Aliases::FILE);
286
287 Aliases::get()
288 .and_then(|x| x.resolve_args(args))
289 .map(|alias| {
290 tokens!(
291 Punct::new('#', Spacing::Joint),
292 Group::new(Delimiter::Bracket, alias),
293 )
294 .chain(item)
295 .collect()
296 })
297 .unwrap_or_else(Error::into_compile_error)
298}
299
300/// Equivalent to [`#[eval]`][macro@eval] but does not have restrictions on
301/// where it can be attached.
302///
303/// # Examples
304///
305/// *Compiled using the [example alias file].*
306///
307/// Non-inline modules can be annotated:
308///
309/// ```
310/// attr_alias::eval_block! {
311/// #[attr_alias(macos, cfg_attr(*, path = "sys/macos.rs"))]
312/// #[attr_alias(macos, cfg_attr(not(*), path = "sys/common.rs"))]
313/// mod sys;
314/// }
315/// ```
316#[cfg_attr(
317 feature = "nightly",
318 doc = "
319Using [`#[eval]`][macro@eval] would require a nightly feature:
320
321```
322#![feature(proc_macro_hygiene)]
323
324#[attr_alias::eval]
325#[attr_alias(macos, cfg_attr(*, path = \"sys/macos.rs\"))]
326#[attr_alias(macos, cfg_attr(not(*), path = \"sys/common.rs\"))]
327mod sys;
328```"
329)]
330///
331/// [example alias file]: self#example
332#[proc_macro]
333pub fn eval_block(item: TokenStream) -> TokenStream {
334 let mut resolved = false;
335 let mut result = eval_item(item, &mut resolved)
336 .unwrap_or_else(Error::into_compile_error);
337
338 let trigger = if resolved {
339 Aliases::create_trigger()
340 } else {
341 Err(Error::new("unnecessary attribute"))
342 };
343 match trigger {
344 Ok(trigger) => result.extend(trigger),
345 Err(error) => result.extend(error.into_compile_error()),
346 }
347
348 result
349}
350
351/// Resolves [`#[attr_alias]`][macro@attr_alias] attributes.
352///
353/// This attribute must be attached to a file-level item. It allows
354/// [`#[attr_alias]`][macro@attr_alias] attributes within that item to be
355/// resolved without nightly features.
356///
357/// # Errors
358///
359/// Errors will typically be clear, but for those that are not, they can be
360/// interpreted as follows:
361/// - *"cannot find attribute `attr_alias` in this scope"* -
362/// The [`#[attr_alias]`][macro@attr_alias] attribute was used without this
363/// attribute or importing it.
364/// - *"`const` items in this context need a name"* -
365/// This attribute was attached to an item that is not at the top level of a
366/// file.
367/// - *"non-inline modules in proc macro input are unstable"* ([E0658]) -
368/// Due to the [proc\_macro\_hygiene] feature being unstable, [`eval_block!`]
369/// should be used instead.
370///
371/// # Examples
372///
373/// *Compiled using the [example alias file].*
374///
375/// **Conditionally Defining a Method:**
376///
377/// ```
378/// # #![cfg_attr(feature = "nightly", feature(doc_cfg))]
379/// #
380/// use std::process::Command;
381///
382/// struct ProcessBuilder(Command);
383///
384/// #[attr_alias::eval]
385/// impl ProcessBuilder {
386/// #[attr_alias(macos_or_windows)]
387#[cfg_attr(
388 feature = "nightly",
389 doc = " #[attr_alias(macos_or_windows, doc(cfg(*)))]"
390)]
391/// fn name(&mut self, name: &str) -> &mut Self {
392/// unimplemented!();
393/// }
394/// }
395/// ```
396#[cfg_attr(
397 feature = "nightly",
398 doc = "
399**Setting Lint Configuration:**
400
401```
402#![feature(custom_inner_attributes)]
403# #![feature(prelude_import)]
404
405#![attr_alias::eval]
406#![attr_alias(warnings, *)]
407```"
408)]
409///
410/// [E0658]: https://doc.rust-lang.org/error_codes/E0658.html
411/// [example alias file]: self#example
412/// [proc\_macro\_hygiene]: https://doc.rust-lang.org/unstable-book/language-features/proc-macro-hygiene.html
413#[proc_macro_attribute]
414pub fn eval(args: TokenStream, item: TokenStream) -> TokenStream {
415 if let Err(error) = parse_empty(args) {
416 return error.into_compile_error();
417 }
418
419 eval_block(item)
420}