use proc_macro::TokenStream;
use quote::quote;
use syn::{MetaNameValue, parse2};
#[derive(Debug)]
struct SinceArg {
since: String,
}
impl syn::parse::Parse for SinceArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let nv: MetaNameValue = input.parse()?;
if !matches!(nv.path.get_ident(), Some(id) if id == "since") {
return Err(syn::Error::new_spanned(
&nv.path,
"expected `since = \"...\"`",
));
}
let since = match nv.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => s.value(),
other => return Err(syn::Error::new_spanned(other, "expected string literal")),
};
Ok(Self { since })
}
}
#[derive(Debug)]
struct FeatureArg {
feature: String,
}
impl syn::parse::Parse for FeatureArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let nv: MetaNameValue = input.parse()?;
if !matches!(nv.path.get_ident(), Some(id) if id == "feature") {
return Err(syn::Error::new_spanned(
&nv.path,
"expected `feature = \"...\"`",
));
}
let feature = match nv.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => s.value(),
other => return Err(syn::Error::new_spanned(other, "expected string literal")),
};
Ok(Self { feature })
}
}
#[derive(Debug)]
struct DeprecArg {
since: String,
note: Option<String>,
}
impl syn::parse::Parse for DeprecArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut since: Option<String> = None;
let mut note: Option<String> = None;
while !input.is_empty() {
let nv: MetaNameValue = input.parse()?;
let value = match nv.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => s.value(),
other => return Err(syn::Error::new_spanned(other, "expected string literal")),
};
match nv.path.get_ident().map(|i| i.to_string()).as_deref() {
Some("since") => since = Some(value),
Some("note") => note = Some(value),
_ => {
return Err(syn::Error::new_spanned(
&nv.path,
"expected `since` or `note`",
));
}
}
if input.peek(syn::Token![,]) {
let _: syn::Token![,] = input.parse()?;
}
}
let since = since.ok_or_else(|| input.error("missing `since = \"...\"`"))?;
Ok(Self { since, note })
}
}
#[proc_macro_attribute]
pub fn stable(args: TokenStream, input: TokenStream) -> TokenStream {
let parsed = match parse2::<SinceArg>(args.into()) {
Ok(p) => p,
Err(e) => return e.to_compile_error().into(),
};
let since_val = parsed.since;
let input: proc_macro2::TokenStream = input.into();
quote! {
#[doc = concat!(" <div class=\"stab stable\"><strong>Stable</strong> since ", #since_val, "</div>")]
#input
}
.into()
}
#[proc_macro_attribute]
pub fn unstable(args: TokenStream, input: TokenStream) -> TokenStream {
let parsed = match parse2::<FeatureArg>(args.into()) {
Ok(p) => p,
Err(e) => return e.to_compile_error().into(),
};
let feature_val = parsed.feature;
let input: proc_macro2::TokenStream = input.into();
quote! {
#[doc = concat!(" <div class=\"stab unstable\"><strong>Unstable</strong> (feature: ", #feature_val, ") — may change or be removed</div>")]
#input
}
.into()
}
#[proc_macro_attribute]
pub fn internal(_args: TokenStream, input: TokenStream) -> TokenStream {
let input: proc_macro2::TokenStream = input.into();
quote! {
#[doc(hidden)]
#input
}
.into()
}
#[proc_macro_attribute]
pub fn deprecated(args: TokenStream, input: TokenStream) -> TokenStream {
let parsed = match parse2::<DeprecArg>(args.into()) {
Ok(p) => p,
Err(e) => return e.to_compile_error().into(),
};
let since_val = parsed.since;
let note_lit = parsed.note.as_deref().unwrap_or("");
let input: proc_macro2::TokenStream = input.into();
quote! {
#[deprecated(since = #since_val, note = #note_lit)]
#[doc = concat!(" <div class=\"stab deprecated\"><strong>Deprecated</strong> since ", #since_val, "</div>")]
#input
}
.into()
}
#[cfg(test)]
mod tests {
use super::{DeprecArg, FeatureArg, SinceArg};
use proc_macro2::TokenStream;
use syn::parse2;
fn ts(s: &str) -> TokenStream {
s.parse().expect("valid token stream")
}
#[test]
fn since_arg_parses() {
let arg: SinceArg = parse2(ts(r#"since = "0.63.0""#)).unwrap();
assert_eq!(arg.since, "0.63.0");
}
#[test]
fn since_arg_rejects_wrong_key() {
let res: syn::Result<SinceArg> = parse2(ts(r#"feature = "browser""#));
assert!(res.is_err(), "expected error for wrong key");
}
#[test]
fn feature_arg_parses() {
let arg: FeatureArg = parse2(ts(r#"feature = "browser""#)).unwrap();
assert_eq!(arg.feature, "browser");
}
#[test]
fn feature_arg_rejects_wrong_key() {
let res: syn::Result<FeatureArg> = parse2(ts(r#"since = "0.63.0""#));
assert!(res.is_err(), "expected error for wrong key");
}
#[test]
fn deprec_arg_only_since() {
let arg: DeprecArg = parse2(ts(r#"since = "0.64.0""#)).unwrap();
assert_eq!(arg.since, "0.64.0");
assert_eq!(arg.note, None);
}
#[test]
fn deprec_arg_since_and_note() {
let arg: DeprecArg = parse2(ts(r#"since = "0.64.0", note = "use new api""#)).unwrap();
assert_eq!(arg.since, "0.64.0");
assert_eq!(arg.note.as_deref(), Some("use new api"));
}
#[test]
fn deprec_arg_note_first_order_independent() {
let arg: DeprecArg = parse2(ts(r#"note = "see docs", since = "0.64.0""#)).unwrap();
assert_eq!(arg.since, "0.64.0");
assert_eq!(arg.note.as_deref(), Some("see docs"));
}
#[test]
fn deprec_arg_missing_since_errors() {
let res: syn::Result<DeprecArg> = parse2(ts(r#"note = "no since""#));
assert!(res.is_err(), "expected error for missing since");
}
#[test]
fn deprec_arg_rejects_unknown_key() {
let res: syn::Result<DeprecArg> = parse2(ts(r#"reason = "nope""#));
assert!(res.is_err(), "expected error for unknown key");
}
#[test]
fn deprec_arg_rejects_non_string_value() {
let res: syn::Result<DeprecArg> = parse2(ts(r#"since = 42"#));
assert!(res.is_err(), "expected error for non-string value");
}
}