use chrono::{Datelike, Local, NaiveDate};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use std::env;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input, LitStr, Token};
#[proc_macro_attribute]
pub fn bestbefore(attr: TokenStream, item: TokenStream) -> TokenStream {
fn compile_error(message: String) -> TokenStream {
let message = syn::LitStr::new(&message, Span::call_site());
quote! {
compile_error!(#message);
}
.into()
}
fn format_date(date: NaiveDate) -> String {
format!("{:02}.{:02}", date.month(), date.year())
}
let attr_args = parse_macro_input!(attr as BestBeforeArgs);
let input = parse_macro_input!(item as syn::Item);
let current_date = env::var("BESTBEFORE_DATE")
.as_deref()
.map(parse_date)
.unwrap_or_else(|_| {
let now = Local::now();
NaiveDate::from_ymd_opt(now.year(), now.month(), 1).unwrap()
});
let item_name = item_name(&input);
if let Some(expires_date) = attr_args.expires_date {
if expires_date != attr_args.warning_date && expires_date <= attr_args.warning_date {
return compile_error(format!(
"Invalid date: expiration date ({}) must be after warning date ({})",
format_date(expires_date),
format_date(attr_args.warning_date)
));
}
if current_date > expires_date {
let message = attr_args.message.unwrap_or_else(|| {
format!(
"Code '{}' has expired (after {}): consider removing this code",
item_name,
format_date(expires_date)
)
});
return compile_error(message);
}
}
let mut result = TokenStream2::new();
if current_date > attr_args.warning_date {
let message = attr_args.message.unwrap_or_else(|| {
format!(
"Code '{}' past warning date ({}): consider updating or removing this code",
item_name,
format_date(attr_args.warning_date)
)
});
let warning = quote! {
#[warn(deprecated)]
#[deprecated(note = #message)]
};
result.extend(warning);
}
result.extend(input.into_token_stream());
result.into()
}
struct BestBeforeArgs {
warning_date: NaiveDate,
expires_date: Option<NaiveDate>,
message: Option<String>,
}
impl Parse for BestBeforeArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut warning_date = None;
let mut expires_date = None;
let mut message = None;
if input.is_empty() {
return Err(syn::Error::new(
input.span(),
"Missing parameters. Expected either warning date or expires parameter",
));
}
if input.peek(LitStr) {
let date_lit: LitStr = input.parse()?;
warning_date = Some(parse_date(&date_lit.value()));
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
while !input.is_empty() {
let name: syn::Ident = input.parse()?;
input.parse::<Token![=]>()?;
if name == "expires" {
let date_lit = input.parse::<LitStr>()?;
expires_date = Some(parse_date(&date_lit.value()));
} else if name == "message" {
let msg_lit = input.parse::<LitStr>()?;
message = Some(msg_lit.value());
} else {
return Err(syn::Error::new(
name.span(),
"Unknown parameter, expected 'expires' or 'message'",
));
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
if warning_date.is_none() {
if let Some(exp_date) = expires_date {
warning_date = Some(exp_date);
} else {
return Err(syn::Error::new(input.span(),
"Missing parameters. You must provide either a warning date or an expires parameter"));
}
}
Ok(BestBeforeArgs {
warning_date: warning_date.unwrap(),
expires_date,
message,
})
}
}
fn parse_date(date_str: &str) -> NaiveDate {
let parts: Vec<&str> = date_str.split('.').collect();
if parts.len() != 2 {
panic!(
"Invalid date format: '{}'. Expected format: 'MM.YYYY'",
date_str
);
}
let month = parts[0].parse::<u32>().unwrap_or_else(|_| {
panic!("Invalid month: '{}'. Expected a number from 1-12", parts[0]);
});
let year = parts[1].parse::<i32>().unwrap_or_else(|_| {
panic!("Invalid year: '{}'. Expected a valid year number", parts[1]);
});
if month < 1 || month > 12 {
panic!("Invalid month: {}. Expected a number from 1-12", month);
}
NaiveDate::from_ymd_opt(year, month, 1).unwrap_or_else(|| {
panic!("Invalid date: {}.{}", month, year);
})
}
fn item_name(item: &syn::Item) -> String {
match item {
syn::Item::Fn(item_fn) => item_fn.sig.ident.to_string(),
syn::Item::Mod(item_mod) => item_mod.ident.to_string(),
syn::Item::Impl(_) => "implementation block".to_string(),
syn::Item::Trait(item_trait) => format!("trait {}", item_trait.ident),
syn::Item::Struct(item_struct) => format!("struct {}", item_struct.ident),
syn::Item::Enum(item_enum) => format!("enum {}", item_enum.ident),
_ => "code block".to_string(),
}
}