#![feature(let_chains)]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(unused_must_use)]
#![deny(unused_extern_crates)]
#![deny(clippy::pedantic)]
#![warn(missing_debug_implementations, unreachable_pub, rustdoc::all)]
mod data_driver;
mod extract;
mod generate;
mod parse;
mod resolve;
mod validate;
use proc_macro::TokenStream;
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::quote;
use syn::{
parse_macro_input, visit::Visit, Attribute, Expr, ExprCall, ExprLit, ExprPath, FnArg,
ImplItemFn, Item, ItemImpl, ItemMod, Lit, Type,
};
#[derive(Clone)]
struct ImportInfo {
name: String,
path: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Receiver {
None,
Ref,
RefMut,
}
struct ParameterInfo {
name: Ident,
ty: TokenStream2,
is_ref: bool,
is_mut_ref: bool,
}
struct FunctionInfo {
name: Ident,
doc: Option<String>,
params: Vec<ParameterInfo>,
input_type: TokenStream2,
output_type: TokenStream2,
is_custom: bool,
returns_ref: bool,
receiver: Receiver,
trait_name: Option<String>,
feed_type: Option<TokenStream2>,
}
struct EventInfo {
topic: String,
data_type: TokenStream2,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DataDriverRole {
EncodeInput,
DecodeInput,
DecodeOutput,
}
struct CustomDataDriverHandler {
fn_name: String,
role: DataDriverRole,
func: syn::ItemFn,
}
struct EmitVisitor {
events: Vec<EventInfo>,
}
impl EmitVisitor {
fn new() -> Self {
Self { events: Vec::new() }
}
}
impl<'ast> Visit<'ast> for EmitVisitor {
fn visit_expr_call(&mut self, node: &'ast ExprCall) {
if let Expr::Path(ExprPath { path, .. }) = &*node.func {
let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
let is_emit = matches!(
segments
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice(),
["abi", "emit"] | ["emit"]
);
if is_emit && node.args.len() >= 2 {
let topic = extract::topic_from_expr(node.args.first().unwrap());
if let Some(topic) = topic {
let data_expr = &node.args[1];
let data_type = extract::type_from_expr(data_expr);
self.events.push(EventInfo { topic, data_type });
}
}
}
syn::visit::visit_expr_call(self, node);
}
}
struct FeedVisitor {
feed_exprs: Vec<String>,
}
impl FeedVisitor {
fn new() -> Self {
Self {
feed_exprs: Vec::new(),
}
}
}
impl<'ast> Visit<'ast> for FeedVisitor {
fn visit_expr_call(&mut self, node: &'ast ExprCall) {
if let Expr::Path(ExprPath { path, .. }) = &*node.func {
let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
let is_feed = matches!(
segments
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice(),
["abi", "feed"] | ["feed"]
);
if is_feed && !node.args.is_empty() {
let expr = &node.args[0];
let expr_str = quote!(#expr).to_string();
self.feed_exprs.push(expr_str);
}
}
syn::visit::visit_expr_call(self, node);
}
}
fn get_feed_exprs(method: &ImplItemFn) -> Vec<String> {
use syn::visit::Visit;
let mut visitor = FeedVisitor::new();
visitor.visit_block(&method.block);
visitor.feed_exprs
}
fn looks_like_tuple(s: &str) -> bool {
let trimmed = s.trim();
trimmed.starts_with('(') && trimmed.contains(',')
}
fn validate_feed_type_match(feed_type_str: &str, feed_exprs: &[String]) -> Option<String> {
if feed_exprs.is_empty() {
return None;
}
let feeds_is_tuple = looks_like_tuple(feed_type_str);
let expr = &feed_exprs[0];
let expr_is_tuple = looks_like_tuple(expr);
if feeds_is_tuple && !expr_is_tuple {
Some(format!(
"feeds attribute specifies tuple type `{feed_type_str}` but expression `{expr}` doesn't look like a tuple"
))
} else if !feeds_is_tuple && expr_is_tuple {
Some(format!(
"feeds attribute specifies non-tuple type `{feed_type_str}` but expression `{expr}` looks like a tuple"
))
} else {
None
}
}
struct ImportExtraction {
imports: Vec<ImportInfo>,
has_glob: bool,
has_relative: bool,
}
struct TraitImplInfo<'a> {
trait_name: String,
impl_block: &'a ItemImpl,
expose_list: Vec<String>,
}
struct ContractData<'a> {
imports: Vec<ImportInfo>,
contract_name: String,
contract_ident: Ident,
impl_blocks: Vec<&'a ItemImpl>,
trait_impls: Vec<TraitImplInfo<'a>>,
custom_handlers: Vec<CustomDataDriverHandler>,
}
fn is_relative_path_keyword(ident: &str) -> bool {
matches!(ident, "self" | "super" | "crate")
}
fn has_empty_body(method: &ImplItemFn) -> bool {
method.block.stmts.is_empty()
}
fn extract_receiver(method: &ImplItemFn) -> Receiver {
if let Some(FnArg::Receiver(receiver)) = method.sig.inputs.first() {
if receiver.mutability.is_some() {
Receiver::RefMut
} else {
Receiver::Ref
}
} else {
Receiver::None
}
}
fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
let docs: Vec<String> = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc")
&& let syn::Meta::NameValue(meta) = &attr.meta
&& let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = &meta.value
{
return Some(s.value().trim().to_string());
}
None
})
.collect();
if docs.is_empty() {
None
} else {
Some(docs.join(" "))
}
}
fn has_custom_attribute(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
if attr.path().is_ident("contract") {
if let Ok(meta) = attr.meta.require_list() {
let tokens = meta.tokens.to_string();
return tokens.contains("custom");
}
}
false
})
}
fn extract_feeds_attribute(attrs: &[Attribute]) -> Option<TokenStream2> {
for attr in attrs {
if !attr.path().is_ident("contract") {
continue;
}
let Ok(meta) = attr.meta.require_list() else {
continue;
};
let tokens = meta.tokens.clone();
let mut iter = tokens.into_iter().peekable();
let Some(proc_macro2::TokenTree::Ident(ident)) = iter.next() else {
continue;
};
if ident != "feeds" {
continue;
}
let Some(proc_macro2::TokenTree::Punct(punct)) = iter.next() else {
continue;
};
if punct.as_char() != '=' {
continue;
}
let Some(proc_macro2::TokenTree::Literal(lit)) = iter.next() else {
continue;
};
let lit_str = lit.to_string();
let type_str = lit_str.trim_matches('"');
if let Ok(ty) = syn::parse_str::<syn::Type>(type_str) {
return Some(quote! { #ty });
}
}
None
}
fn generate_arg_expr(param: &ParameterInfo) -> TokenStream2 {
let name = ¶m.name;
if param.is_mut_ref {
quote! { &mut #name }
} else if param.is_ref {
quote! { &#name }
} else {
quote! { #name }
}
}
#[proc_macro_attribute]
pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream {
let module = parse_macro_input!(item as ItemMod);
let Some((_, items)) = &module.content else {
return syn::Error::new_spanned(&module, "#[contract] requires a module with content")
.to_compile_error()
.into();
};
let data = match extract::contract_data(&module, items) {
Ok(data) => data,
Err(e) => return e.to_compile_error().into(),
};
let ContractData {
imports,
contract_name,
contract_ident,
impl_blocks,
trait_impls,
custom_handlers,
} = data;
let mut functions = Vec::new();
let mut events = Vec::new();
for impl_block in &impl_blocks {
match extract::public_methods(impl_block) {
Ok(methods) => functions.extend(methods),
Err(e) => return e.to_compile_error().into(),
}
events.extend(extract::emit_calls(impl_block));
}
for trait_impl in &trait_impls {
match extract::trait_methods(trait_impl) {
Ok(trait_functions) => functions.extend(trait_functions),
Err(e) => return e.to_compile_error().into(),
}
events.extend(extract::emit_calls(trait_impl.impl_block));
}
let mut seen = std::collections::HashSet::new();
let events: Vec<_> = events
.into_iter()
.filter(|e| seen.insert(e.topic.clone()))
.collect();
let schema = generate::schema(&contract_name, &imports, &functions, &events);
let state_static = generate::state_static(&contract_ident);
let externs = generate::extern_wrappers(&functions, &contract_ident);
let type_map = resolve::build_type_map(&imports, &functions, &events);
let data_driver = data_driver::module(&type_map, &functions, &events, &custom_handlers);
let mod_vis = &module.vis;
let mod_name = &module.ident;
let mod_attrs = &module.attrs;
let new_items: Vec<_> = items
.iter()
.filter(|item| !extract::is_custom_handler(item))
.map(|item| {
if let Item::Impl(impl_block) = item
&& let Type::Path(type_path) = &*impl_block.self_ty
&& type_path.path.is_ident(&contract_name)
{
Item::Impl(generate::strip_contract_attributes(impl_block.clone()))
} else {
item.clone()
}
})
.collect();
let output = quote! {
#schema
#[cfg(not(feature = "data-driver"))]
#(#mod_attrs)*
#mod_vis mod #mod_name {
#(#new_items)*
#state_static
#externs
}
#data_driver
};
output.into()
}
#[cfg(test)]
mod tests {
use super::*;
use syn::visit::Visit;
#[test]
fn test_emit_visitor_finds_emit_call() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn pause(&mut self) {
self.is_paused = true;
abi::emit("paused", PauseEvent {});
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 1);
assert_eq!(visitor.events[0].topic, "paused");
}
#[test]
fn test_emit_visitor_finds_const_topic() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn pause(&mut self) {
abi::emit(events::PauseToggled::PAUSED, events::PauseToggled());
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 1);
assert_eq!(visitor.events[0].topic, "events::PauseToggled::PAUSED");
}
#[test]
fn test_emit_visitor_multiple_emits() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn transfer(&mut self) {
abi::emit("started", StartEvent {});
abi::emit("completed", CompleteEvent {});
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 2);
}
#[test]
fn test_emit_visitor_nested_in_if() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn maybe_emit(&mut self, condition: bool) {
if condition {
abi::emit("conditional", Event {});
}
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 1);
assert_eq!(visitor.events[0].topic, "conditional");
}
#[test]
fn test_emit_visitor_nested_in_loop() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn emit_many(&mut self, items: Vec<u32>) {
for item in items {
abi::emit("item_processed", ItemEvent { value: item });
}
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 1);
}
#[test]
fn test_emit_visitor_just_emit_without_abi_prefix() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn do_something(&mut self) {
emit("event", SomeEvent {});
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 1);
assert_eq!(visitor.events[0].topic, "event");
}
#[test]
fn test_emit_visitor_no_emit_calls() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn get_value(&self) -> u64 {
self.value
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 0);
}
#[test]
fn test_emit_visitor_across_multiple_methods() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyContract {
pub fn pause(&mut self) {
abi::emit("paused", PauseEvent {});
}
pub fn unpause(&mut self) {
abi::emit("unpaused", UnpauseEvent {});
}
}
};
let mut visitor = EmitVisitor::new();
visitor.visit_item_impl(&impl_block);
assert_eq!(visitor.events.len(), 2);
}
#[test]
fn test_extract_doc_comment_single_line() {
let attrs: Vec<Attribute> = vec![syn::parse_quote!(#[doc = " First line."])];
let doc = extract_doc_comment(&attrs);
assert!(doc.is_some());
assert_eq!(doc.unwrap(), "First line.");
}
#[test]
fn test_extract_doc_comment_multiple_lines() {
let attrs: Vec<Attribute> = vec![
syn::parse_quote!(#[doc = " First line."]),
syn::parse_quote!(#[doc = " Second line."]),
];
let doc = extract_doc_comment(&attrs);
assert!(doc.is_some());
let doc = doc.unwrap();
assert!(doc.contains("First line"));
assert!(doc.contains("Second line"));
}
#[test]
fn test_extract_doc_comment_none() {
let attrs: Vec<Attribute> = vec![syn::parse_quote!(#[inline])];
let doc = extract_doc_comment(&attrs);
assert!(doc.is_none());
}
#[test]
fn test_extract_doc_comment_empty() {
let attrs: Vec<Attribute> = vec![];
let doc = extract_doc_comment(&attrs);
assert!(doc.is_none());
}
#[test]
fn test_extract_doc_comment_mixed_attrs() {
let attrs: Vec<Attribute> = vec![
syn::parse_quote!(#[inline]),
syn::parse_quote!(#[doc = " The doc comment."]),
syn::parse_quote!(#[allow(unused)]),
];
let doc = extract_doc_comment(&attrs);
assert!(doc.is_some());
assert_eq!(doc.unwrap(), "The doc comment.");
}
#[test]
fn test_has_custom_attribute_true() {
let attrs: Vec<Attribute> = vec![syn::parse_quote!(#[contract(custom)])];
assert!(has_custom_attribute(&attrs));
}
#[test]
fn test_has_custom_attribute_false() {
let attrs: Vec<Attribute> = vec![syn::parse_quote!(#[doc = "Some doc"])];
assert!(!has_custom_attribute(&attrs));
}
#[test]
fn test_has_custom_attribute_empty() {
let attrs: Vec<Attribute> = vec![];
assert!(!has_custom_attribute(&attrs));
}
#[test]
fn test_has_custom_attribute_other_contract_attr() {
let attrs: Vec<Attribute> = vec![syn::parse_quote!(#[contract(expose = [foo])])];
assert!(!has_custom_attribute(&attrs));
}
#[test]
fn test_has_custom_attribute_mixed() {
let attrs: Vec<Attribute> = vec![
syn::parse_quote!(#[doc = "Some doc"]),
syn::parse_quote!(#[contract(custom)]),
syn::parse_quote!(#[inline]),
];
assert!(has_custom_attribute(&attrs));
}
}