1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#![forbid(missing_docs)]
#![doc = include_str!("../README.md")]
#![warn(clippy::pedantic)]

use std::collections::HashMap;

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::parse::Parse;
use syn::{parse_macro_input, Ident, LitStr, Token};

struct Args {
    since: Option<String>,
    note: Option<String>,
    remove: String,
}

impl Parse for Args {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mut args = ["since", "note", "remove"]
            .into_iter()
            .map(|s| (String::from(s), None))
            .collect::<HashMap<_, _>>();
        while !input.is_empty() {
            let (ident, _, value) = (
                input.parse::<Ident>()?,
                input.parse::<Token![=]>()?,
                input.parse::<LitStr>()?,
            );
            match args.insert(ident.to_string(), Some(value.value())) {
                None => {
                    return Err(syn::Error::new(
                        ident.span(),
                        format!("unknown meta item '{ident}'"),
                    ))
                }
                Some(Some(_)) => {
                    return Err(syn::Error::new(
                        ident.span(),
                        format!("duplicate '{ident}' items"),
                    ))
                }
                Some(None) => (),
            }
            if !input.is_empty() {
                input.parse::<Token![,]>()?;
            }
        }
        if let Some(Some(remove)) = args.remove("remove") {
            Ok(Args {
                since: args.remove("since").unwrap(),
                note: args.remove("note").unwrap(),
                remove,
            })
        } else {
            Err(syn::Error::new(
                Span::call_site(),
                "mandatory 'remove' item missing",
            ))
        }
    }
}

impl ToTokens for Args {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let removal = format!("removal scheduled for version {}", self.remove);
        let note = match &self.note {
            Some(n) => format!("{n} ({removal})"),
            None => removal,
        };
        let args = if let Some(since) = &self.since {
            quote!(since = #since, note = #note)
        } else {
            quote!(note = #note)
        };
        tokens.extend(quote!(#[deprecated(#args)]));
    }
}

macro_rules! error {
    ($fmt:literal $(,$args:expr)*$(,)?) => {{
        let error = format!($fmt $(,$args)*);
        return quote! { compile_error!(#error); }.into();
    }}
}

/// Similar to Rust `deprecated` attribute, with the addition of a mandatory
/// `remove` attribute argument which contains a semver requirement at which
/// the item must be definitely removed from the source code.
///
/// See the [crate level documentation](self) for a complete description.
#[proc_macro_attribute]
pub fn deprecate_until(args: TokenStream, tokens: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as Args);
    let remove = match semver::VersionReq::parse(&args.remove) {
        Ok(c) => c,
        Err(e) => error!("version '{}' cannot be parsed: {e:?}", args.remove),
    };
    if let Ok(version) = std::env::var("CARGO_PKG_VERSION") {
        let version = match semver::Version::parse(&version) {
            Ok(v) => v,
            Err(e) => error!("unable to parse current version '{version}': {e:?}"),
        };
        if remove.matches(&version) {
            error!("version '{version}' matches '{remove}', item should be removed");
        }
    }
    let tokens: proc_macro2::TokenStream = tokens.into();
    quote!(#args #tokens).into()
}