allow_until/
lib.rs

1//! # `allow-until`
2//!
3//! Allows an item until a specified semver version, and then errors on compilation.
4//!
5//! [![github]](https://github.com/DexterHill0/allow-until) [![crates-io]](https://crates.io/crates/allow-until) [![docs-rs]](https://docs.rs/allow-until)
6//!
7//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
8//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
9//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
10//!
11//! ```rust
12//! #[allow_until(version = ">= 1.0.x", reason = "struct is deprecated from version 1.0.x onwards")]
13//! struct MyStruct {
14//!     //....
15//! }
16//! ```
17//! Or with the derive macro:
18//! ```rust
19//! #[derive(AllowUntil)]
20//! struct MyStruct {
21//!     #[allow_until(version = ">= 1.0.x", reason = "member is deprecated from version 1.0.x onwards")]
22//!     foo: usize
23//! }
24//! ```
25
26#![feature(proc_macro_diagnostic, proc_macro_span)]
27
28use proc_macro::{TokenTree as TT, *};
29use semver::{Version, VersionReq};
30
31struct Args {
32    pub version: VersionReq,
33    pub reason: Option<String>,
34}
35
36fn parse_arguments(args: TokenStream) -> Result<Args, Diagnostic> {
37    let mut toks = args.into_iter().peekable();
38
39    let mut version = None;
40    let mut reason = None;
41
42    while let Some(tok) = toks.next() {
43        let ident = match tok {
44            TT::Ident(ident) => ident,
45            t => {
46                return Err(t
47                    .span()
48                    .error("expected ident")
49                    .help("valid arguments are `version` and `reason`"));
50            }
51        };
52
53        match toks.next() {
54            Some(TT::Punct(p)) if p.as_char() == '=' => (),
55            Some(t) => return Err(t.span().error("expected `=`")),
56            None => {
57                return Err(Span::call_site()
58                    .error("unexpected end of tokens")
59                    .help("expected `=`"))
60            }
61        }
62
63        let lit = match toks.next() {
64            Some(TT::Literal(lit)) => lit,
65            Some(t) => return Err(t.span().error("expected literal")),
66            None => {
67                return Err(Span::call_site()
68                    .error("unexpected end of tokens")
69                    .help("expected literal"))
70            }
71        };
72
73        match &ident.to_string()[..] {
74            "version" => {
75                let lit_str = lit.to_string();
76
77                let v = lit_str
78                    .get(1..lit_str.len() - 1)
79                    .ok_or(lit.span().error("expected string literal"))?;
80
81                version = Some(
82                    VersionReq::parse(v).map_err(|_| lit.span().error("invalid semver version"))?,
83                );
84            }
85            "reason" => {
86                let lit_str = lit.to_string();
87
88                let v = lit_str
89                    .get(1..lit_str.len() - 1)
90                    .ok_or(lit.span().error("expected string literal"))?;
91
92                reason = Some(v.into());
93            }
94            _ => {
95                return Err(lit
96                    .span()
97                    .error("unknown argument")
98                    .help("valid arguments are `version` and `reason`"))
99            }
100        }
101
102        match toks.peek() {
103            Some(TT::Punct(p)) if p.as_char() == ',' => {
104                toks.next();
105            }
106            Some(t) => {
107                return Err(t
108                    .span()
109                    .error("unexpected token")
110                    .help("expected end of tokens or `,`"))
111            }
112            None => {}
113        }
114    }
115
116    if version.is_none() {
117        return Err(Span::call_site().error("missing required `version` argument"));
118    }
119
120    Ok(Args {
121        reason,
122        version: version.unwrap(),
123    })
124}
125
126fn emit_error_version_match(pred: VersionReq, reason: Option<String>, at: Span) {
127    if let Ok(pkg_ver) = std::env::var("CARGO_PKG_VERSION") {
128        let version = Version::parse(&pkg_ver).expect("invalid cargo semver ver");
129
130        if pred.matches(&version) {
131            at.error(reason.map_or(
132                format!("item not allowed! (version {} matches {})", version, pred),
133                |r| format!("{} (version {} matches {})", r, version, pred),
134            ))
135            .emit();
136        }
137    }
138}
139
140fn recurse_find_attr(group: Group) {
141    let mut toks = group.stream().into_iter();
142
143    loop {
144        match toks.next() {
145            Some(TT::Group(g)) => recurse_find_attr(g),
146            Some(TT::Punct(hash)) if hash.as_char() == '#' => match toks.next() {
147                Some(TT::Group(inner_g)) => {
148                    let mut toks = inner_g.stream().into_iter();
149
150                    match toks.next() {
151                        Some(TT::Ident(ident)) if &ident.to_string()[..] == "allow_until" => {
152                            match toks.next() {
153                                Some(TT::Group(g)) => {
154                                    let args = parse_arguments(g.stream());
155                                    let args = match args {
156                                        Err(e) => {
157                                            e.emit();
158                                            return;
159                                        }
160                                        Ok(a) => a,
161                                    };
162
163                                    emit_error_version_match(
164                                        args.version,
165                                        args.reason,
166                                        hash.span()
167                                            .join(inner_g.span())
168                                            .unwrap()
169                                            .join(ident.span())
170                                            .unwrap(),
171                                    );
172
173                                    continue;
174                                }
175                                _ => continue,
176                            }
177                        }
178                        _ => continue,
179                    }
180                }
181                _ => continue,
182            },
183            None => break,
184            _ => continue,
185        }
186    }
187}
188
189/// Allows an item until a specified semver version, and then errors on compilation.
190///
191/// ```rust
192/// #[allow_until(version = ">= 1.0.x", reason = "struct is deprecated from version 1.0.x onwards")]
193/// struct MyStruct {
194///     //....
195/// }
196/// ```
197#[proc_macro_attribute]
198pub fn allow_until(args: TokenStream, input: TokenStream) -> TokenStream {
199    let args = parse_arguments(args);
200
201    let args = match args {
202        Err(e) => {
203            e.emit();
204            return input;
205        }
206        Ok(a) => a,
207    };
208
209    emit_error_version_match(args.version, args.reason, Span::call_site());
210
211    input
212}
213
214/// Allows an item until a specified semver version, and then errors on compilation.
215///
216/// ```rust
217/// #[derive(AllowUntil)]
218/// struct MyStruct {
219///     #[allow_until(version = ">= 1.0.x", reason = "member is deprecated from version 1.0.x onwards")]
220///     foo: usize
221/// }
222/// ```
223#[proc_macro_derive(AllowUntil, attributes(allow_until))]
224pub fn allow_until_derive(stream: TokenStream) -> TokenStream {
225    let toks = stream.into_iter();
226
227    for tok in toks {
228        match tok {
229            TT::Group(g) => recurse_find_attr(g),
230            _ => continue,
231        }
232    }
233
234    TokenStream::new()
235}