1use darling::{ast::NestedMeta, Error, FromMeta};
2use indoc::formatdoc;
3use proc_macro2::TokenStream;
4use quote::{quote, ToTokens};
5use syn::{parse_quote, Item};
6
7use crate::item_like::{ItemLike, Stability};
8
9pub fn unstable_macro(args: TokenStream, input: TokenStream) -> TokenStream {
10 let attributes = match NestedMeta::parse_meta_list(args) {
11 Ok(attributes) => attributes,
12 Err(err) => return Error::from(err).write_errors(),
13 };
14 let unstable_attribute = match UnstableAttribute::from_list(&attributes) {
15 Ok(attributes) => attributes,
16 Err(err) => return err.write_errors(),
17 };
18 match syn::parse2::<Item>(input) {
19 Ok(item) => match item {
20 Item::Type(item_type) => unstable_attribute.expand(item_type),
21 Item::Enum(item_enum) => unstable_attribute.expand(item_enum),
22 Item::Struct(item_struct) => unstable_attribute.expand(item_struct),
23 Item::Fn(item_fn) => unstable_attribute.expand(item_fn),
24 Item::Mod(item_mod) => unstable_attribute.expand(item_mod),
25 Item::Trait(item_trait) => unstable_attribute.expand(item_trait),
26 Item::Const(item_const) => unstable_attribute.expand(item_const),
27 Item::Static(item_static) => unstable_attribute.expand(item_static),
28 Item::Use(item_use) => unstable_attribute.expand(item_use),
29 Item::Impl(item_impl) => unstable_attribute.expand_impl(item_impl),
30 _ => panic!("unsupported item type"),
31 },
32 Err(err) => Error::from(err).write_errors(),
33 }
34}
35
36#[derive(Debug, Default, FromMeta)]
37pub struct UnstableAttribute {
38 feature: Option<String>,
42
43 issue: Option<String>,
47}
48
49impl UnstableAttribute {
50 pub fn expand(&self, mut item: impl ItemLike + ToTokens + Clone) -> TokenStream {
51 if !item.is_public() {
52 return item.into_token_stream();
54 }
55
56 let feature_flag = self.feature_flag();
57 self.add_doc(&mut item);
58
59 let mut hidden_item = item.clone();
60 hidden_item.set_visibility(parse_quote! { pub(crate) });
61
62 let allows = item
63 .allowed_lints()
64 .into_iter()
65 .map(|ident| quote! { #[allow(#ident)] });
66
67 quote! {
68 #[cfg(any(doc, feature = #feature_flag))]
69 #[cfg_attr(docsrs, doc(cfg(feature = #feature_flag)))]
70 #item
71
72 #[cfg(not(any(doc, feature = #feature_flag)))]
73 #(#allows)*
74 #hidden_item
75 }
76 }
77
78 pub fn expand_impl(&self, mut item: impl Stability + ToTokens) -> TokenStream {
79 let feature_flag = self.feature_flag();
80 self.add_doc(&mut item);
81 quote! {
82 #[cfg(any(doc, feature = #feature_flag))]
83 #[cfg_attr(docsrs, doc(cfg(feature = #feature_flag)))]
84 #item
85 }
86 }
87
88 fn add_doc(&self, item: &mut impl Stability) {
89 let feature_flag = self.feature_flag();
90 let doc = formatdoc! {"
91 # Stability
92
93 **This API is marked as unstable** and is only available when the `{feature_flag}`
94 crate feature is enabled. This comes with no stability guarantees, and could be changed
95 or removed at any time."};
96 item.push_attr(parse_quote! { #[doc = #doc] });
97
98 if let Some(issue) = &self.issue {
99 let doc = format!("The tracking issue is: `{}`.", issue);
100 item.push_attr(parse_quote! { #[doc = #doc] });
101 }
102 }
103
104 fn feature_flag(&self) -> String {
105 self.feature
106 .as_deref()
107 .map_or(String::from("unstable"), |name| format!("unstable-{name}"))
108 }
109}
110#[cfg(test)]
111mod tests {
112 use pretty_assertions::assert_eq;
113 use quote::quote;
114 use syn::parse_quote;
115
116 use super::*;
117
118 #[test]
119 fn unstable_feature_flag_default() {
120 let unstable = UnstableAttribute::default();
121 assert_eq!(unstable.feature_flag(), "unstable");
122 }
123
124 #[test]
125 fn unstable_feature_flag_with_feature() {
126 let unstable = UnstableAttribute {
127 feature: Some("experimental".to_string()),
128 issue: None,
129 };
130 assert_eq!(unstable.feature_flag(), "unstable-experimental");
131 }
132
133 #[test]
134 fn expand_non_public_item() {
135 let item: syn::ItemStruct = parse_quote! {
136 struct MyStruct;
137 };
138 let unstable = UnstableAttribute::default();
139 let tokens = unstable.expand(item.clone());
140 assert_eq!(tokens.to_string(), quote! { struct MyStruct; }.to_string());
141 }
142
143 const DEFAULT_DOC: &str = "# Stability\n\n**This API is marked as unstable** and is only available when the `unstable`\ncrate feature is enabled. This comes with no stability guarantees, and could be changed\nor removed at any time.";
144 const WITH_FEATURES_DOC: &str = "# Stability\n\n**This API is marked as unstable** and is only available when the `unstable-experimental`\ncrate feature is enabled. This comes with no stability guarantees, and could be changed\nor removed at any time.";
145 const ISSUE_DOC: &str = "The tracking issue is: `#123`.";
146
147 #[test]
148 fn expand_with_feature() {
149 let item: syn::ItemType = parse_quote! { pub type Foo = Bar; };
150 let unstable = UnstableAttribute {
151 feature: Some("experimental".to_string()),
152 issue: None,
153 };
154 let tokens = unstable.expand(item);
155 let expected = quote! {
156 #[cfg(any(doc, feature = "unstable-experimental"))]
157 #[cfg_attr(docsrs, doc(cfg(feature = "unstable-experimental")))]
158 #[doc = #WITH_FEATURES_DOC]
159 pub type Foo = Bar;
160
161 #[cfg(not(any(doc, feature = "unstable-experimental")))]
162 #[allow(dead_code)]
163 #[doc = #WITH_FEATURES_DOC]
164 pub(crate) type Foo = Bar;
165 };
166 assert_eq!(tokens.to_string(), expected.to_string());
167 }
168
169 #[test]
170 fn expand_with_issue() {
171 let item: syn::ItemType = parse_quote! { pub type Foo = Bar; };
172 let unstable = UnstableAttribute {
173 feature: None,
174 issue: Some("#123".to_string()),
175 };
176 let tokens = unstable.expand(item);
177 let expected = quote! {
178 #[cfg(any(doc, feature = "unstable"))]
179 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
180 #[doc = #DEFAULT_DOC]
181 #[doc = #ISSUE_DOC]
182 pub type Foo = Bar;
183
184 #[cfg(not(any(doc, feature = "unstable")))]
185 #[allow(dead_code)]
186 #[doc = #DEFAULT_DOC]
187 #[doc = #ISSUE_DOC]
188 pub(crate) type Foo = Bar;
189 };
190 assert_eq!(tokens.to_string(), expected.to_string());
191 }
192
193 #[test]
194 fn expand_public_type() {
195 let item: syn::ItemType = parse_quote! { pub type Foo = Bar; };
196 let tokens = UnstableAttribute::default().expand(item);
197 let expected = quote! {
198 #[cfg(any(doc, feature = "unstable"))]
199 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
200 #[doc = #DEFAULT_DOC]
201 pub type Foo = Bar;
202
203 #[cfg(not(any(doc, feature = "unstable")))]
204 #[allow(dead_code)]
205 #[doc = #DEFAULT_DOC]
206 pub(crate) type Foo = Bar;
207 };
208 assert_eq!(tokens.to_string(), expected.to_string());
209 }
210
211 #[test]
212 fn expand_public_struct() {
213 let item: syn::ItemStruct = parse_quote! {
214 pub struct Foo {
215 pub field: i32,
216 }
217 };
218 let tokens = UnstableAttribute::default().expand(item);
219 let expected = quote! {
220 #[cfg(any(doc, feature = "unstable"))]
221 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
222 #[doc = #DEFAULT_DOC]
223 pub struct Foo {
224 pub field: i32,
225 }
226
227 #[cfg(not(any(doc, feature = "unstable")))]
228 #[allow(dead_code)]
229 #[doc = #DEFAULT_DOC]
230 pub(crate) struct Foo {
231 pub (crate) field: i32,
232 }
233 };
234 assert_eq!(tokens.to_string(), expected.to_string());
235 }
236
237 #[test]
238 fn expand_public_enum() {
239 let item: syn::ItemEnum = parse_quote! {
240 pub enum Foo {
241 A,
242 B,
243 }
244 };
245 let tokens = UnstableAttribute::default().expand(item);
246 let expected = quote! {
247 #[cfg(any(doc, feature = "unstable"))]
248 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
249 #[doc = #DEFAULT_DOC]
250 pub enum Foo {
251 A,
252 B,
253 }
254
255 #[cfg(not(any(doc, feature = "unstable")))]
256 #[allow(dead_code)]
257 #[doc = #DEFAULT_DOC]
258 pub(crate) enum Foo {
259 A,
260 B,
261 }
262 };
263 assert_eq!(tokens.to_string(), expected.to_string());
264 }
265
266 #[test]
267 fn expand_public_fn() {
268 let item: syn::ItemFn = parse_quote! {
269 pub fn foo() {}
270 };
271 let tokens = UnstableAttribute::default().expand(item);
272 let expected = quote! {
273 #[cfg(any(doc, feature = "unstable"))]
274 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
275 #[doc = #DEFAULT_DOC]
276 pub fn foo() {}
277
278 #[cfg(not(any(doc, feature = "unstable")))]
279 #[allow(dead_code)]
280 #[doc = #DEFAULT_DOC]
281 pub(crate) fn foo() {}
282 };
283 assert_eq!(tokens.to_string(), expected.to_string());
284 }
285
286 #[test]
287 fn expand_public_trait() {
288 let item: syn::ItemTrait = parse_quote! {
289 pub trait Foo {
290 fn bar(&self);
291 }
292 };
293 let tokens = UnstableAttribute::default().expand(item);
294 let expected = quote! {
295 #[cfg(any(doc, feature = "unstable"))]
296 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
297 #[doc = #DEFAULT_DOC]
298 pub trait Foo {
299 fn bar(&self);
300 }
301
302 #[cfg(not(any(doc, feature = "unstable")))]
303 #[allow(dead_code)]
304 #[doc = #DEFAULT_DOC]
305 pub(crate) trait Foo {
306 fn bar(&self);
307 }
308 };
309 assert_eq!(tokens.to_string(), expected.to_string());
310 }
311
312 #[test]
313 fn expand_public_const() {
314 let item: syn::ItemConst = parse_quote! {
315 pub const FOO: i32 = 42;
316 };
317 let tokens = UnstableAttribute::default().expand(item);
318 let expected = quote! {
319 #[cfg(any(doc, feature = "unstable"))]
320 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
321 #[doc = #DEFAULT_DOC]
322 pub const FOO: i32 = 42;
323
324 #[cfg(not(any(doc, feature = "unstable")))]
325 #[allow(dead_code)]
326 #[doc = #DEFAULT_DOC]
327 pub(crate) const FOO: i32 = 42;
328 };
329 assert_eq!(tokens.to_string(), expected.to_string());
330 }
331
332 #[test]
333 fn expand_public_static() {
334 let item: syn::ItemStatic = parse_quote! {
335 pub static FOO: i32 = 42;
336 };
337 let tokens = UnstableAttribute::default().expand(item);
338 let expected = quote! {
339 #[cfg(any(doc, feature = "unstable"))]
340 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
341 #[doc = #DEFAULT_DOC]
342 pub static FOO: i32 = 42;
343
344 #[cfg(not(any(doc, feature = "unstable")))]
345 #[allow(dead_code)]
346 #[doc = #DEFAULT_DOC]
347 pub(crate) static FOO: i32 = 42;
348 };
349 assert_eq!(tokens.to_string(), expected.to_string());
350 }
351
352 #[test]
353 fn expand_public_mod() {
354 let item: syn::ItemMod = parse_quote! {
355 pub mod foo {
356 pub fn bar() {}
357 }
358 };
359 let tokens = UnstableAttribute::default().expand(item);
360 let expected = quote! {
361 #[cfg(any(doc, feature = "unstable"))]
362 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
363 #[doc = #DEFAULT_DOC]
364 pub mod foo {
365 pub fn bar() {}
366 }
367
368 #[cfg(not(any(doc, feature = "unstable")))]
369 #[allow(dead_code)]
370 #[doc = #DEFAULT_DOC]
371 pub(crate) mod foo {
372 pub fn bar() {}
373 }
374 };
375 assert_eq!(tokens.to_string(), expected.to_string());
376 }
377
378 #[test]
379 fn expand_public_use() {
380 let item: syn::ItemUse = parse_quote! {
381 pub use crate::foo::bar;
382 };
383 let tokens = UnstableAttribute::default().expand(item);
384 let expected = quote! {
385 #[cfg(any(doc, feature = "unstable"))]
386 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
387 #[doc = #DEFAULT_DOC]
388 pub use crate::foo::bar;
389
390 #[cfg(not(any(doc, feature = "unstable")))]
391 #[allow(unused_imports)]
392 #[doc = #DEFAULT_DOC]
393 pub(crate) use crate::foo::bar;
394 };
395 assert_eq!(tokens.to_string(), expected.to_string());
396 }
397
398 #[test]
399 fn expand_impl_block() {
400 let item: syn::ItemImpl = parse_quote! {
401 impl Default for crate::foo::Foo {}
402 };
403 let tokens = UnstableAttribute::default().expand_impl(item);
404 let expected = quote! {
405 #[cfg(any(doc, feature = "unstable"))]
406 #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
407 #[doc = #DEFAULT_DOC]
408 impl Default for crate::foo::Foo {}
409 };
410 assert_eq!(tokens.to_string(), expected.to_string());
411 }
412}