instability/
stable.rs

1use darling::{ast::NestedMeta, Error, FromMeta};
2use indoc::formatdoc;
3use proc_macro2::TokenStream;
4use quote::ToTokens;
5use syn::{parse_quote, Item};
6
7use crate::item_like::{ItemLike, Stability};
8
9pub fn stable_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 StableAttribute::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 StableAttribute {
38    /// The version at which the item was stabilized.
39    since: Option<String>,
40
41    /// A link or reference to a tracking issue for the feature.
42    issue: Option<String>,
43}
44
45impl StableAttribute {
46    pub fn expand(&self, item: impl ItemLike + ToTokens + Clone) -> TokenStream {
47        if !item.is_public() {
48            // We only care about public items.
49            return item.into_token_stream();
50        }
51        self.expand_impl(item)
52    }
53
54    pub fn expand_impl(&self, mut item: impl Stability + ToTokens) -> TokenStream {
55        let doc = if let Some(ref version) = self.since {
56            formatdoc! {"
57                # Stability
58
59                This API was stabilized in version {}.",
60                version.trim_start_matches('v')
61            }
62        } else {
63            formatdoc! {"
64                # Stability
65
66                This API is stable."}
67        };
68        item.push_attr(parse_quote! { #[doc = #doc] });
69
70        if let Some(issue) = &self.issue {
71            let doc = format!("The tracking issue is: `{}`.", issue);
72            item.push_attr(parse_quote! { #[doc = #doc] });
73        }
74        item.into_token_stream()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use pretty_assertions::assert_eq;
81    use quote::quote;
82    use syn::parse_quote;
83
84    use super::*;
85
86    #[test]
87    fn expand_non_public_item() {
88        let item: syn::ItemStruct = parse_quote! {
89            struct MyStruct;
90        };
91        let stable = StableAttribute::default();
92        let tokens = stable.expand(item.clone());
93        assert_eq!(tokens.to_string(), quote! { struct MyStruct; }.to_string());
94    }
95
96    const STABLE_DOC: &str = "# Stability\n\nThis API is stable.";
97    const SINCE_DOC: &str = "# Stability\n\nThis API was stabilized in version 1.0.0.";
98    const ISSUE_DOC: &str = "The tracking issue is: `#123`.";
99
100    #[test]
101    fn expand_with_since() {
102        let item: syn::ItemType = parse_quote! { pub type Foo = Bar; };
103        let stable = StableAttribute {
104            since: Some("v1.0.0".to_string()),
105            issue: None,
106        };
107        let tokens = stable.expand(item);
108        let expected = quote! {
109            #[doc = #SINCE_DOC]
110            pub type Foo = Bar;
111        };
112        assert_eq!(tokens.to_string(), expected.to_string());
113    }
114
115    #[test]
116    fn expand_with_issue() {
117        let item: syn::ItemType = parse_quote! { pub type Foo = Bar; };
118        let stable = StableAttribute {
119            since: None,
120            issue: Some("#123".to_string()),
121        };
122        let tokens = stable.expand(item);
123        let expected = quote! {
124            #[doc = #STABLE_DOC]
125            #[doc = #ISSUE_DOC]
126            pub type Foo = Bar;
127        };
128        assert_eq!(tokens.to_string(), expected.to_string());
129    }
130
131    #[test]
132    fn expand_with_since_and_issue() {
133        let item: syn::ItemType = parse_quote! { pub type Foo = Bar; };
134        let stable = StableAttribute {
135            since: Some("v1.0.0".to_string()),
136            issue: Some("#123".to_string()),
137        };
138        let tokens = stable.expand(item);
139        let expected = quote! {
140            #[doc = #SINCE_DOC]
141            #[doc = #ISSUE_DOC]
142            pub type Foo = Bar;
143        };
144        assert_eq!(tokens.to_string(), expected.to_string());
145    }
146
147    #[test]
148    fn expand_public_type() {
149        let item: syn::ItemType = parse_quote! { pub type Foo = Bar; };
150        let stable = StableAttribute::default();
151        let tokens = stable.expand(item);
152        let expected = quote! {
153            #[doc = #STABLE_DOC]
154            pub type Foo = Bar;
155        };
156        assert_eq!(tokens.to_string(), expected.to_string());
157    }
158
159    #[test]
160    fn expand_public_struct() {
161        let item: syn::ItemStruct = parse_quote! {
162            pub struct Foo {
163                pub field: i32,
164            }
165        };
166        let stable = StableAttribute::default();
167        let tokens = stable.expand(item);
168        let expected = quote! {
169            #[doc = #STABLE_DOC]
170            pub struct Foo {
171                pub field: i32,
172            }
173        };
174        assert_eq!(tokens.to_string(), expected.to_string());
175    }
176
177    #[test]
178    fn expand_public_enum() {
179        let item: syn::ItemEnum = parse_quote! {
180            pub enum Foo {
181                A,
182                B,
183            }
184        };
185        let stable = StableAttribute::default();
186        let tokens = stable.expand(item);
187        let expected = quote! {
188            #[doc = #STABLE_DOC]
189            pub enum Foo {
190                A,
191                B,
192            }
193        };
194        assert_eq!(tokens.to_string(), expected.to_string());
195    }
196
197    #[test]
198    fn expand_public_fn() {
199        let item: syn::ItemFn = parse_quote! {
200            pub fn foo() {}
201        };
202        let stable = StableAttribute::default();
203        let tokens = stable.expand(item);
204        let expected = quote! {
205            #[doc = #STABLE_DOC]
206            pub fn foo() {}
207        };
208        assert_eq!(tokens.to_string(), expected.to_string());
209    }
210
211    #[test]
212    fn expand_public_trait() {
213        let item: syn::ItemTrait = parse_quote! {
214            pub trait Foo {
215                fn bar(&self);
216            }
217        };
218        let stable = StableAttribute::default();
219        let tokens = stable.expand(item);
220        let expected = quote! {
221            #[doc = #STABLE_DOC]
222            pub trait Foo {
223                fn bar(&self);
224            }
225        };
226        assert_eq!(tokens.to_string(), expected.to_string());
227    }
228
229    #[test]
230    fn expand_public_const() {
231        let item: syn::ItemConst = parse_quote! {
232            pub const FOO: i32 = 42;
233        };
234        let stable = StableAttribute::default();
235        let tokens = stable.expand(item);
236        let expected = quote! {
237            #[doc = #STABLE_DOC]
238            pub const FOO: i32 = 42;
239        };
240        assert_eq!(tokens.to_string(), expected.to_string());
241    }
242
243    #[test]
244    fn expand_public_static() {
245        let item: syn::ItemStatic = parse_quote! {
246            pub static FOO: i32 = 42;
247        };
248        let stable = StableAttribute::default();
249        let tokens = stable.expand(item);
250        let expected = quote! {
251            #[doc = #STABLE_DOC]
252            pub static FOO: i32 = 42;
253        };
254        assert_eq!(tokens.to_string(), expected.to_string());
255    }
256
257    #[test]
258    fn expand_public_mod() {
259        let item: syn::ItemMod = parse_quote! {
260            pub mod foo {
261                pub fn bar() {}
262            }
263        };
264        let stable = StableAttribute::default();
265        let tokens = stable.expand(item);
266        let expected = quote! {
267            #[doc = #STABLE_DOC]
268            pub mod foo {
269                pub fn bar() {}
270            }
271        };
272        assert_eq!(tokens.to_string(), expected.to_string());
273    }
274
275    #[test]
276    fn expand_public_use() {
277        let item: syn::ItemUse = parse_quote! {
278            pub use crate::foo::bar;
279        };
280        let stable = StableAttribute::default();
281        let tokens = stable.expand(item);
282        let expected = quote! {
283            #[doc = #STABLE_DOC]
284            pub use crate::foo::bar;
285        };
286        assert_eq!(tokens.to_string(), expected.to_string());
287    }
288
289    #[test]
290    fn expand_impl_block() {
291        let item: syn::ItemImpl = parse_quote! {
292            impl Default for crate::foo::Foo {}
293        };
294        let tokens = StableAttribute::default().expand_impl(item);
295        let expected = quote! {
296            #[doc = #STABLE_DOC]
297            impl Default for crate::foo::Foo {}
298        };
299        assert_eq!(tokens.to_string(), expected.to_string());
300    }
301}