1#![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#[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#[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}