use quote::ToTokens;
use syn::spanned::Spanned;
use syn::visit::Visit;
use syn::{Attribute, Expr, ExprMacro, Item, ItemMacro, Macro, Stmt, StmtMacro};
#[derive(Debug, Clone)]
pub struct MacroCall {
pub name: String,
pub kind: MacroKind,
pub line: usize,
pub col_start: usize,
pub col_end: usize,
pub line_end: usize,
pub item_line_end: usize,
pub input: String,
pub arguments: String,
pub sibling_derives: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MacroKind {
Functional,
Attribute,
Derive,
}
impl MacroKind {
pub fn as_str(&self) -> &'static str {
match self {
MacroKind::Functional => "fn",
MacroKind::Attribute => "attr",
MacroKind::Derive => "derive",
}
}
}
fn get_item_attrs_mut(item: &mut Item) -> Option<&mut Vec<Attribute>> {
match item {
Item::Const(i) => Some(&mut i.attrs),
Item::Enum(i) => Some(&mut i.attrs),
Item::ExternCrate(i) => Some(&mut i.attrs),
Item::Fn(i) => Some(&mut i.attrs),
Item::ForeignMod(i) => Some(&mut i.attrs),
Item::Impl(i) => Some(&mut i.attrs),
Item::Macro(i) => Some(&mut i.attrs),
Item::Mod(i) => Some(&mut i.attrs),
Item::Static(i) => Some(&mut i.attrs),
Item::Struct(i) => Some(&mut i.attrs),
Item::Trait(i) => Some(&mut i.attrs),
Item::TraitAlias(i) => Some(&mut i.attrs),
Item::Type(i) => Some(&mut i.attrs),
Item::Union(i) => Some(&mut i.attrs),
Item::Use(i) => Some(&mut i.attrs),
_ => None,
}
}
fn item_without_attr(item: &Item, attr_idx: usize) -> String {
let mut item = item.clone();
if let Some(attrs) = get_item_attrs_mut(&mut item) {
if attr_idx < attrs.len() {
attrs.remove(attr_idx);
}
}
item.to_token_stream().to_string()
}
struct MacroVisitor {
macros: Vec<MacroCall>,
}
impl MacroVisitor {
fn new() -> Self {
Self { macros: Vec::new() }
}
fn add_macro_from_path(&mut self, mac: &Macro, kind: MacroKind) {
let name = mac
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
let start_span = mac.path.segments.first().map(|s| s.ident.span());
let end_span = mac.delimiter.span().close();
if name == "macro_rules" {
return;
}
if let Some(start) = start_span {
let line = start.start().line;
let col_start = start.start().column;
let line_end = end_span.end().line;
let col_end = end_span.end().column;
let input = mac.tokens.to_string();
self.macros.push(MacroCall {
name,
kind,
line,
col_start,
col_end,
line_end,
item_line_end: line_end,
input,
arguments: String::new(),
sibling_derives: Vec::new(),
});
}
}
fn process_attributes(&mut self, attrs: &[Attribute], item: &Item) {
let item_span = item.to_token_stream().into_iter().last();
let item_line_end = item_span
.map(|t| t.span().end().line)
.unwrap_or(0);
for (attr_idx, attr) in attrs.iter().enumerate() {
let name = attr
.path()
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
let attr_span = attr.span();
let line = attr_span.start().line;
let col_start = attr_span.start().column;
let line_end = attr_span.end().line;
let col_end = attr_span.end().column;
if name == "derive" {
let input = item_without_attr(item, attr_idx);
if let syn::Meta::List(list) = &attr.meta {
let tokens_str = list.tokens.to_string();
let all_derive_names: Vec<String> = tokens_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
for derive_name in &all_derive_names {
self.macros.push(MacroCall {
name: derive_name.clone(),
kind: MacroKind::Derive,
line,
col_start,
col_end,
line_end,
item_line_end,
input: input.clone(),
arguments: String::new(),
sibling_derives: all_derive_names.clone(),
});
}
}
} else {
let input = item_without_attr(item, attr_idx);
let arguments = match &attr.meta {
syn::Meta::List(list) => list.tokens.to_string(),
syn::Meta::NameValue(nv) => nv.value.to_token_stream().to_string(),
syn::Meta::Path(_) => String::new(),
};
self.macros.push(MacroCall {
name,
kind: MacroKind::Attribute,
line,
col_start,
col_end,
line_end,
item_line_end,
input,
arguments,
sibling_derives: Vec::new(),
});
}
}
}
}
impl<'a> Visit<'a> for MacroVisitor {
fn visit_item(&mut self, item: &'a Item) {
match item {
Item::Const(i) => self.process_attributes(&i.attrs, item),
Item::Enum(i) => self.process_attributes(&i.attrs, item),
Item::ExternCrate(i) => self.process_attributes(&i.attrs, item),
Item::Fn(i) => self.process_attributes(&i.attrs, item),
Item::ForeignMod(i) => self.process_attributes(&i.attrs, item),
Item::Impl(i) => self.process_attributes(&i.attrs, item),
Item::Macro(i) => self.process_attributes(&i.attrs, item),
Item::Mod(i) => self.process_attributes(&i.attrs, item),
Item::Static(i) => self.process_attributes(&i.attrs, item),
Item::Struct(i) => self.process_attributes(&i.attrs, item),
Item::Trait(i) => self.process_attributes(&i.attrs, item),
Item::TraitAlias(i) => self.process_attributes(&i.attrs, item),
Item::Type(i) => self.process_attributes(&i.attrs, item),
Item::Union(i) => self.process_attributes(&i.attrs, item),
Item::Use(i) => self.process_attributes(&i.attrs, item),
_ => {}
}
syn::visit::visit_item(self, item);
}
fn visit_item_macro(&mut self, node: &'a ItemMacro) {
self.add_macro_from_path(&node.mac, MacroKind::Functional);
syn::visit::visit_item_macro(self, node);
}
fn visit_expr_macro(&mut self, node: &'a ExprMacro) {
self.add_macro_from_path(&node.mac, MacroKind::Functional);
syn::visit::visit_expr_macro(self, node);
}
fn visit_expr(&mut self, expr: &'a Expr) {
if let Expr::Macro(m) = expr {
self.add_macro_from_path(&m.mac, MacroKind::Functional);
}
syn::visit::visit_expr(self, expr);
}
fn visit_stmt(&mut self, stmt: &'a Stmt) {
if let Stmt::Macro(m) = stmt {
self.add_macro_from_path(&m.mac, MacroKind::Functional);
}
syn::visit::visit_stmt(self, stmt);
}
fn visit_stmt_macro(&mut self, node: &'a StmtMacro) {
self.add_macro_from_path(&node.mac, MacroKind::Functional);
syn::visit::visit_stmt_macro(self, node);
}
}
pub fn is_builtin_attribute(name: &str) -> bool {
matches!(
name,
"doc"
| "cfg"
| "cfg_attr"
| "allow"
| "warn"
| "deny"
| "forbid"
| "deprecated"
| "must_use"
| "repr"
| "inline"
| "cold"
| "track_caller"
| "link"
| "link_name"
| "link_section"
| "no_mangle"
| "used"
| "path"
| "non_exhaustive"
| "automatically_derived"
| "global_allocator"
| "export_name"
| "macro_use"
| "macro_export"
)
}
fn is_builtin_functional(name: &str) -> bool {
matches!(
name,
"include_str"
| "include_bytes"
| "include"
| "env"
| "option_env"
| "concat"
| "stringify"
| "line"
| "column"
| "file"
| "module_path"
| "cfg"
| "compile_error"
| "format_args"
| "format_args_nl"
)
}
pub fn find_macros(source: &str) -> Vec<MacroCall> {
let file = match syn::parse_file(source) {
Ok(f) => f,
Err(_) => return Vec::new(),
};
let mut visitor = MacroVisitor::new();
visitor.visit_file(&file);
visitor.macros.sort_by_key(|m| m.line);
visitor.macros.retain(|m| match m.kind {
MacroKind::Functional => !is_builtin_functional(&m.name),
MacroKind::Attribute => !is_builtin_attribute(&m.name),
MacroKind::Derive => true, });
let mut seen = std::collections::HashSet::new();
visitor
.macros
.into_iter()
.filter(|m| seen.insert((m.line, m.name.clone(), m.kind)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_functional_macros() {
let source = r#"
fn main() {
println!("Hello");
vec![1, 2, 3];
}
"#;
let macros = find_macros(source);
let names: Vec<_> = macros.iter().map(|m| m.name.as_str()).collect();
assert!(names.contains(&"println"));
assert!(names.contains(&"vec"));
}
#[test]
fn test_functional_macro_columns() {
let source = "fn test() {\n let v = vec![1, 2, 3];\n}";
let macros = find_macros(source);
let vec_mac = macros.iter().find(|m| m.name == "vec").unwrap();
assert_eq!(vec_mac.line, 2);
assert_eq!(vec_mac.col_start, 12);
assert_eq!(vec_mac.col_end, 25);
assert_eq!(vec_mac.line_end, 2);
let line = source.lines().nth(vec_mac.line - 1).unwrap();
assert_eq!(&line[..vec_mac.col_start], " let v = ");
assert_eq!(&line[vec_mac.col_end..], ";");
}
#[test]
fn test_find_attribute_macros() {
let source = r#"
#[test]
fn my_test() {}
#[my_custom_attr]
fn custom() {}
"#;
let macros = find_macros(source);
let names: Vec<_> = macros.iter().map(|m| m.name.as_str()).collect();
assert!(names.contains(&"test"));
assert!(names.contains(&"my_custom_attr"));
let source2 = r#"
#[cfg(test)]
mod tests {}
#[doc = "hello"]
fn documented() {}
#[allow(unused)]
fn linted() {}
"#;
let macros2 = find_macros(source2);
let names2: Vec<_> = macros2.iter().map(|m| m.name.as_str()).collect();
assert!(!names2.contains(&"cfg"), "cfg should be filtered as built-in");
assert!(!names2.contains(&"doc"), "doc should be filtered as built-in");
assert!(!names2.contains(&"allow"), "allow should be filtered as built-in");
}
#[test]
fn test_find_derive_macros() {
let source = r#"
#[derive(Debug, Clone, PartialEq)]
struct Foo {
x: i32,
}
"#;
let macros = find_macros(source);
let names: Vec<_> = macros.iter().map(|m| m.name.as_str()).collect();
assert!(!names.contains(&"derive"), "derive attribute itself should be skipped");
assert!(names.contains(&"Debug"));
assert!(names.contains(&"Clone"));
assert!(names.contains(&"PartialEq"));
}
#[test]
fn test_multiple_attribute_macros_on_one_item() {
let source = r#"
#[attr_a]
#[attr_b(x, y)]
pub struct Multi {
val: i32,
}
"#;
let macros = find_macros(source);
let attrs: Vec<_> = macros
.iter()
.filter(|m| m.kind == MacroKind::Attribute)
.collect();
assert_eq!(attrs.len(), 2, "Expected 2 attribute macros: {:?}", attrs);
let a = attrs.iter().find(|m| m.name == "attr_a").unwrap();
let b = attrs.iter().find(|m| m.name == "attr_b").unwrap();
assert_eq!(a.line, 2);
assert_eq!(a.item_line_end, 6);
assert_eq!(b.line, 3);
assert_eq!(b.item_line_end, 6);
assert!(
a.input.contains("attr_b"),
"attr_a input should retain #[attr_b]: {}",
a.input
);
assert!(
!a.input.contains("attr_a"),
"attr_a input should NOT contain #[attr_a]: {}",
a.input
);
assert!(
b.input.contains("attr_a"),
"attr_b input should retain #[attr_a]: {}",
b.input
);
assert!(
!b.input.contains("attr_b"),
"attr_b input should NOT contain #[attr_b]: {}",
b.input
);
assert_eq!(b.arguments, "x , y");
assert_eq!(a.arguments, "");
}
#[test]
fn test_multiple_derives_one_attribute() {
let source = r#"
#[derive(Debug, Clone, PartialEq)]
struct Bar {
x: i32,
}
"#;
let macros = find_macros(source);
let derives: Vec<_> = macros
.iter()
.filter(|m| m.kind == MacroKind::Derive)
.collect();
assert_eq!(derives.len(), 3, "Expected 3 derive macros: {:?}", derives);
for d in &derives {
assert_eq!(d.line, 2, "derive {} line", d.name);
assert_eq!(d.item_line_end, 5, "derive {} item_line_end", d.name);
}
let debug = derives.iter().find(|m| m.name == "Debug").unwrap();
assert!(
!debug.input.contains("derive"),
"derive input should not contain #[derive]: {}",
debug.input
);
assert!(
debug.input.contains("struct Bar"),
"derive input should contain the struct: {}",
debug.input
);
}
#[test]
fn test_multiple_derives_two_attributes() {
let source = r#"
#[derive(Debug)]
#[derive(Clone, PartialEq)]
struct Baz;
"#;
let macros = find_macros(source);
let derives: Vec<_> = macros
.iter()
.filter(|m| m.kind == MacroKind::Derive)
.collect();
assert_eq!(derives.len(), 3, "Expected 3 derive macros: {:?}", derives);
let debug = derives.iter().find(|m| m.name == "Debug").unwrap();
assert_eq!(debug.line, 2);
assert_eq!(debug.item_line_end, 4);
let clone = derives.iter().find(|m| m.name == "Clone").unwrap();
let partialeq = derives.iter().find(|m| m.name == "PartialEq").unwrap();
assert_eq!(clone.line, 3);
assert_eq!(clone.item_line_end, 4);
assert_eq!(partialeq.line, 3);
assert_eq!(partialeq.item_line_end, 4);
assert!(
!debug.input.contains("Debug"),
"Debug input should not contain its own derive: {}",
debug.input
);
assert!(
debug.input.contains("Clone"),
"Debug input should retain the other derive attr: {}",
debug.input
);
assert!(
clone.input.contains("Debug"),
"Clone input should retain #[derive(Debug)]: {}",
clone.input
);
assert!(
!clone.input.contains("Clone"),
"Clone input should not contain its own derive: {}",
clone.input
);
}
}