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}