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 since: Option<String>,
40
41 issue: Option<String>,
43}
44
45impl StableAttribute {
46 pub fn expand(&self, item: impl ItemLike + ToTokens + Clone) -> TokenStream {
47 if !item.is_public() {
48 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}