use std::path::PathBuf;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{LitStr, Token};
#[proc_macro_attribute]
pub fn contract(attr: TokenStream, item: TokenStream) -> TokenStream {
let item = proc_macro2::TokenStream::from(item);
let names = match Punctuated::<LitStr, Token![,]>::parse_terminated.parse(attr) {
Ok(names) if !names.is_empty() => names,
Ok(_) => {
return syn::Error::new(
Span::call_site(),
"name the obligations this test proves: #[impish::contract(\"<primitive>/A<n>\")]",
)
.into_compile_error()
.into();
}
Err(e) => return e.into_compile_error().into(),
};
let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let mut readmes = Vec::new();
let mut errors = Vec::new();
for name in &names {
match check(&root, &name.value()) {
Ok(readme) => readmes.push(readme),
Err(problem) => errors.push(syn::Error::new(name.span(), problem).into_compile_error()),
}
}
quote! {
#(#errors)*
#(const _: &[u8] = include_bytes!(#readmes);)*
#[test]
#item
}
.into()
}
fn check(root: &std::path::Path, tag: &str) -> Result<String, String> {
let (primitive, id) = tag
.rsplit_once("/A")
.and_then(|(p, n)| Some((p, n.parse::<u32>().ok()?)))
.ok_or_else(|| format!("`{tag}` isn't <primitive>/A<n>"))?;
let plain = |c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_';
if primitive.is_empty() || !primitive.chars().all(plain) {
return Err(format!(
"`{tag}`: the primitive must be letters, digits, `-` and `_`"
));
}
let file = format!("contract/{primitive}/README.md");
let path = root.join(&file);
let text =
std::fs::read_to_string(&path).map_err(|_| format!("`{tag}`: {file} can't be read"))?;
let declared = impish_core::obligations(&text, &file).map_err(|_| {
format!("`{tag}`: {file} isn't a valid Contract; `cargo impish check` names its problems")
})?;
match declared.get(&id) {
None => Err(format!("`{tag}`: {file} doesn't declare A{id}")),
Some(o) if o.retired => Err(format!("`{tag}`: {file} declares A{id} retired")),
Some(_) => Ok(path.display().to_string()),
}
}