oxicode_api_stability/
lib.rs1use proc_macro::TokenStream;
11use quote::quote;
12use syn::{MetaNameValue, parse2};
13
14#[derive(Debug)]
16struct SinceArg {
17 since: String,
18}
19impl syn::parse::Parse for SinceArg {
20 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
21 let nv: MetaNameValue = input.parse()?;
22 if !matches!(nv.path.get_ident(), Some(id) if id == "since") {
23 return Err(syn::Error::new_spanned(
24 &nv.path,
25 "expected `since = \"...\"`",
26 ));
27 }
28 let since = match nv.value {
29 syn::Expr::Lit(syn::ExprLit {
30 lit: syn::Lit::Str(s),
31 ..
32 }) => s.value(),
33 other => return Err(syn::Error::new_spanned(other, "expected string literal")),
34 };
35 Ok(Self { since })
36 }
37}
38
39#[derive(Debug)]
41struct FeatureArg {
42 feature: String,
43}
44impl syn::parse::Parse for FeatureArg {
45 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
46 let nv: MetaNameValue = input.parse()?;
47 if !matches!(nv.path.get_ident(), Some(id) if id == "feature") {
48 return Err(syn::Error::new_spanned(
49 &nv.path,
50 "expected `feature = \"...\"`",
51 ));
52 }
53 let feature = match nv.value {
54 syn::Expr::Lit(syn::ExprLit {
55 lit: syn::Lit::Str(s),
56 ..
57 }) => s.value(),
58 other => return Err(syn::Error::new_spanned(other, "expected string literal")),
59 };
60 Ok(Self { feature })
61 }
62}
63
64#[derive(Debug)]
67struct DeprecArg {
68 since: String,
69 note: Option<String>,
70}
71impl syn::parse::Parse for DeprecArg {
72 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
73 let mut since: Option<String> = None;
74 let mut note: Option<String> = None;
75 while !input.is_empty() {
77 let nv: MetaNameValue = input.parse()?;
78 let value = match nv.value {
79 syn::Expr::Lit(syn::ExprLit {
80 lit: syn::Lit::Str(s),
81 ..
82 }) => s.value(),
83 other => return Err(syn::Error::new_spanned(other, "expected string literal")),
84 };
85 match nv.path.get_ident().map(|i| i.to_string()).as_deref() {
86 Some("since") => since = Some(value),
87 Some("note") => note = Some(value),
88 _ => {
89 return Err(syn::Error::new_spanned(
90 &nv.path,
91 "expected `since` or `note`",
92 ));
93 }
94 }
95 if input.peek(syn::Token![,]) {
96 let _: syn::Token![,] = input.parse()?;
97 }
98 }
99 let since = since.ok_or_else(|| input.error("missing `since = \"...\"`"))?;
100 Ok(Self { since, note })
101 }
102}
103
104#[proc_macro_attribute]
110pub fn stable(args: TokenStream, input: TokenStream) -> TokenStream {
111 let parsed = match parse2::<SinceArg>(args.into()) {
112 Ok(p) => p,
113 Err(e) => return e.to_compile_error().into(),
114 };
115 let since_val = parsed.since;
116 let input: proc_macro2::TokenStream = input.into();
117 quote! {
118 #[doc = concat!(" <div class=\"stab stable\"><strong>Stable</strong> since ", #since_val, "</div>")]
119 #input
120 }
121 .into()
122}
123
124#[proc_macro_attribute]
130pub fn unstable(args: TokenStream, input: TokenStream) -> TokenStream {
131 let parsed = match parse2::<FeatureArg>(args.into()) {
132 Ok(p) => p,
133 Err(e) => return e.to_compile_error().into(),
134 };
135 let feature_val = parsed.feature;
136 let input: proc_macro2::TokenStream = input.into();
137 quote! {
138 #[doc = concat!(" <div class=\"stab unstable\"><strong>Unstable</strong> (feature: ", #feature_val, ") — may change or be removed</div>")]
139 #input
140 }
141 .into()
142}
143
144#[proc_macro_attribute]
146pub fn internal(_args: TokenStream, input: TokenStream) -> TokenStream {
147 let input: proc_macro2::TokenStream = input.into();
148 quote! {
149 #[doc(hidden)]
150 #input
151 }
152 .into()
153}
154
155#[proc_macro_attribute]
164pub fn deprecated(args: TokenStream, input: TokenStream) -> TokenStream {
165 let parsed = match parse2::<DeprecArg>(args.into()) {
166 Ok(p) => p,
167 Err(e) => return e.to_compile_error().into(),
168 };
169 let since_val = parsed.since;
170 let note_lit = parsed.note.as_deref().unwrap_or("");
171 let input: proc_macro2::TokenStream = input.into();
172 quote! {
173 #[deprecated(since = #since_val, note = #note_lit)]
174 #[doc = concat!(" <div class=\"stab deprecated\"><strong>Deprecated</strong> since ", #since_val, "</div>")]
175 #input
176 }
177 .into()
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{DeprecArg, FeatureArg, SinceArg};
193 use proc_macro2::TokenStream;
194 use syn::parse2;
195
196 fn ts(s: &str) -> TokenStream {
197 s.parse().expect("valid token stream")
198 }
199
200 #[test]
201 fn since_arg_parses() {
202 let arg: SinceArg = parse2(ts(r#"since = "0.63.0""#)).unwrap();
203 assert_eq!(arg.since, "0.63.0");
204 }
205
206 #[test]
207 fn since_arg_rejects_wrong_key() {
208 let res: syn::Result<SinceArg> = parse2(ts(r#"feature = "browser""#));
209 assert!(res.is_err(), "expected error for wrong key");
210 }
211
212 #[test]
213 fn feature_arg_parses() {
214 let arg: FeatureArg = parse2(ts(r#"feature = "browser""#)).unwrap();
215 assert_eq!(arg.feature, "browser");
216 }
217
218 #[test]
219 fn feature_arg_rejects_wrong_key() {
220 let res: syn::Result<FeatureArg> = parse2(ts(r#"since = "0.63.0""#));
221 assert!(res.is_err(), "expected error for wrong key");
222 }
223
224 #[test]
225 fn deprec_arg_only_since() {
226 let arg: DeprecArg = parse2(ts(r#"since = "0.64.0""#)).unwrap();
227 assert_eq!(arg.since, "0.64.0");
228 assert_eq!(arg.note, None);
229 }
230
231 #[test]
232 fn deprec_arg_since_and_note() {
233 let arg: DeprecArg = parse2(ts(r#"since = "0.64.0", note = "use new api""#)).unwrap();
234 assert_eq!(arg.since, "0.64.0");
235 assert_eq!(arg.note.as_deref(), Some("use new api"));
236 }
237
238 #[test]
239 fn deprec_arg_note_first_order_independent() {
240 let arg: DeprecArg = parse2(ts(r#"note = "see docs", since = "0.64.0""#)).unwrap();
241 assert_eq!(arg.since, "0.64.0");
242 assert_eq!(arg.note.as_deref(), Some("see docs"));
243 }
244
245 #[test]
246 fn deprec_arg_missing_since_errors() {
247 let res: syn::Result<DeprecArg> = parse2(ts(r#"note = "no since""#));
248 assert!(res.is_err(), "expected error for missing since");
249 }
250
251 #[test]
252 fn deprec_arg_rejects_unknown_key() {
253 let res: syn::Result<DeprecArg> = parse2(ts(r#"reason = "nope""#));
254 assert!(res.is_err(), "expected error for unknown key");
255 }
256
257 #[test]
258 fn deprec_arg_rejects_non_string_value() {
259 let res: syn::Result<DeprecArg> = parse2(ts(r#"since = 42"#));
260 assert!(res.is_err(), "expected error for non-string value");
261 }
262}