instability/
unstable.rs

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    /// The name of the feature that enables the unstable API.
39    ///
40    /// If not specified, the item will instead be guarded by a catch-all `unstable` feature.
41    feature: Option<String>,
42
43    /// A link or reference to a tracking issue for the unstable feature.
44    ///
45    /// This will be included in the item's documentation.
46    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            // We only care about public items.
53            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}