use std::collections::HashMap;
use syn::{File, Item};
const STANDARD_DERIVES: &[&str] = &[
"Debug",
"Clone",
"Copy",
"PartialEq",
"Eq",
"PartialOrd",
"Ord",
"Hash",
"Default",
"Display",
"From",
"Into",
"TryFrom",
"TryInto",
"AsRef",
"AsMut",
"Deref",
"DerefMut",
"Error",
];
#[derive(Clone)]
pub struct MacroRulesInfo {
pub name: String,
pub is_exported: bool,
pub usage_count: usize,
#[allow(dead_code)]
pub item: syn::ItemMacro,
}
impl std::fmt::Debug for MacroRulesInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MacroRulesInfo")
.field("name", &self.name)
.field("is_exported", &self.is_exported)
.field("usage_count", &self.usage_count)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct DeriveInfo {
#[allow(dead_code)]
pub type_name: String,
pub derives: Vec<String>,
pub custom_derives: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
#[allow(dead_code)]
pub enum MacroPlacement {
TopLevel,
WithType(String),
Standalone,
}
pub struct MacroAnalyzer {
pub macro_rules: Vec<MacroRulesInfo>,
pub derive_usage: HashMap<String, DeriveInfo>,
pub attribute_macros: Vec<String>,
}
impl MacroAnalyzer {
pub fn new() -> Self {
Self {
macro_rules: Vec::new(),
derive_usage: HashMap::new(),
attribute_macros: Vec::new(),
}
}
pub fn analyze_file(&mut self, file: &File) {
let mut macro_invocations: HashMap<String, usize> = HashMap::new();
for item in &file.items {
self.count_macro_invocations_in_item(item, &mut macro_invocations);
}
for item in &file.items {
match item {
Item::Macro(item_macro) => {
self.process_macro_rules(item_macro, ¯o_invocations);
}
Item::Struct(s) => {
self.process_derives_on_attrs(&s.ident.to_string(), &s.attrs);
self.process_attribute_macros(&s.attrs);
}
Item::Enum(e) => {
self.process_derives_on_attrs(&e.ident.to_string(), &e.attrs);
self.process_attribute_macros(&e.attrs);
}
_ => {}
}
}
}
#[allow(dead_code)]
pub fn get_required_derives(&self, type_name: &str) -> &[String] {
self.derive_usage
.get(type_name)
.map(|d| d.derives.as_slice())
.unwrap_or(&[])
}
#[allow(dead_code)]
pub fn is_macro_exported(&self, name: &str) -> bool {
self.macro_rules
.iter()
.any(|m| m.name == name && m.is_exported)
}
#[allow(dead_code)]
pub fn suggest_macro_placement(&self, macro_name: &str) -> MacroPlacement {
let macro_info = self.macro_rules.iter().find(|m| m.name == macro_name);
if let Some(info) = macro_info {
if info.is_exported {
return MacroPlacement::TopLevel;
}
}
let lower_macro = macro_name.to_lowercase();
let stripped_macro = lower_macro.replace('_', "");
for type_name in self.derive_usage.keys() {
let lower_type = type_name.to_lowercase();
let stripped_type = lower_type.replace('_', "");
let direct_match = lower_macro.contains(lower_type.as_str())
|| lower_type.contains(lower_macro.as_str());
let stripped_match = stripped_macro.contains(stripped_type.as_str())
|| stripped_type.contains(stripped_macro.as_str());
if direct_match || stripped_match {
return MacroPlacement::WithType(type_name.clone());
}
}
MacroPlacement::Standalone
}
pub fn exported_macro_count(&self) -> usize {
self.macro_rules.iter().filter(|m| m.is_exported).count()
}
pub fn total_macro_count(&self) -> usize {
self.macro_rules.len()
}
pub fn all_custom_derives(&self) -> Vec<String> {
let mut custom: Vec<String> = Vec::new();
for info in self.derive_usage.values() {
for d in &info.custom_derives {
if !custom.contains(d) {
custom.push(d.clone());
}
}
}
custom
}
}
impl MacroAnalyzer {
fn process_macro_rules(
&mut self,
item_macro: &syn::ItemMacro,
invocations: &HashMap<String, usize>,
) {
let path_ident = match item_macro.mac.path.get_ident() {
Some(id) => id,
None => return,
};
if path_ident != "macro_rules" {
return;
}
let defined_name = match &item_macro.ident {
Some(id) => id.to_string(),
None => return,
};
let is_exported = item_macro.attrs.iter().any(|attr| {
attr.path()
.get_ident()
.is_some_and(|id| id == "macro_export")
});
let usage_count = invocations.get(&defined_name).copied().unwrap_or(0);
self.macro_rules.push(MacroRulesInfo {
name: defined_name,
is_exported,
usage_count,
item: item_macro.clone(),
});
}
fn process_derives_on_attrs(&mut self, type_name: &str, attrs: &[syn::Attribute]) {
for attr in attrs {
let is_derive = attr.path().get_ident().is_some_and(|id| id == "derive");
if !is_derive {
continue;
}
let meta_list = match attr.meta.require_list() {
Ok(ml) => ml,
Err(_) => continue,
};
let tokens_str = meta_list.tokens.to_string();
let derives: Vec<String> = tokens_str
.split(',')
.map(|s| {
s.split_whitespace().collect::<Vec<_>>().join("")
})
.filter(|s| !s.is_empty())
.collect();
let custom_derives: Vec<String> = derives
.iter()
.filter(|d| !STANDARD_DERIVES.contains(&d.as_str()))
.cloned()
.collect();
let entry = self
.derive_usage
.entry(type_name.to_string())
.or_insert_with(|| DeriveInfo {
type_name: type_name.to_string(),
derives: Vec::new(),
custom_derives: Vec::new(),
});
entry.derives.extend(derives);
entry.custom_derives.extend(custom_derives);
}
}
fn process_attribute_macros(&mut self, attrs: &[syn::Attribute]) {
const BUILTIN: &[&str] = &[
"derive",
"cfg",
"allow",
"deny",
"warn",
"must_use",
"deprecated",
"doc",
"inline",
"repr",
"test",
"cfg_attr",
"automatically_derived",
"non_exhaustive",
"macro_export",
"macro_use",
"path",
"recursion_limit",
"feature",
"global_allocator",
"no_std",
"no_mangle",
"export_name",
"link_section",
"used",
"cold",
"track_caller",
];
for attr in attrs {
let path_str = attr
.path()
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::");
if !BUILTIN.contains(&path_str.as_str()) && !self.attribute_macros.contains(&path_str) {
self.attribute_macros.push(path_str);
}
}
}
fn count_macro_invocations_in_item(&self, item: &Item, counts: &mut HashMap<String, usize>) {
use quote::ToTokens;
let token_str = item.to_token_stream().to_string();
let tokens: Vec<&str> = token_str.split_whitespace().collect();
let len = tokens.len();
for i in 0..len.saturating_sub(1) {
let next = tokens[i + 1];
if next == "!" || next.starts_with('!') {
let candidate = tokens[i].trim_end_matches('!');
if !candidate.is_empty()
&& candidate.chars().all(|c| c.is_alphanumeric() || c == '_')
&& candidate
.chars()
.next()
.is_some_and(|c| !c.is_ascii_digit())
{
*counts.entry(candidate.to_string()).or_insert(0) += 1;
}
}
}
}
}
impl Default for MacroAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_creates_empty_analyzer() {
let analyzer = MacroAnalyzer::new();
assert!(analyzer.macro_rules.is_empty());
assert!(analyzer.derive_usage.is_empty());
assert!(analyzer.attribute_macros.is_empty());
assert_eq!(analyzer.total_macro_count(), 0);
assert_eq!(analyzer.exported_macro_count(), 0);
}
#[test]
fn test_analyze_file_detects_macro_rules() {
let code = r#"
macro_rules! my_vec {
($($x:expr),*) => { vec![$($x),*] };
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
assert_eq!(analyzer.total_macro_count(), 1);
assert_eq!(analyzer.macro_rules[0].name, "my_vec");
assert!(!analyzer.macro_rules[0].is_exported);
}
#[test]
fn test_analyze_file_detects_derives_on_struct() {
let code = r#"
#[derive(Debug, Clone, serde::Serialize)]
struct Config {
value: i32,
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
let info = analyzer
.derive_usage
.get("Config")
.expect("Config not found in derive_usage");
assert!(
info.derives.iter().any(|d| d == "Debug"),
"Expected Debug in derives"
);
assert!(
info.derives.iter().any(|d| d == "Clone"),
"Expected Clone in derives"
);
assert!(
!info.custom_derives.is_empty(),
"Expected at least one custom derive"
);
}
#[test]
fn test_is_macro_exported() {
let code = r#"
#[macro_export]
macro_rules! public_macro {
() => {};
}
macro_rules! private_macro {
() => {};
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
assert!(
analyzer.is_macro_exported("public_macro"),
"public_macro should be exported"
);
assert!(
!analyzer.is_macro_exported("private_macro"),
"private_macro should not be exported"
);
assert_eq!(analyzer.exported_macro_count(), 1);
}
#[test]
fn test_suggest_placement_top_level_for_exported() {
let code = r#"
#[macro_export]
macro_rules! crate_macro {
() => {};
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
assert_eq!(
analyzer.suggest_macro_placement("crate_macro"),
MacroPlacement::TopLevel
);
}
#[test]
fn test_suggest_placement_with_type_on_name_overlap() {
let code = r#"
#[derive(Debug)]
struct MyStruct {
x: i32,
}
macro_rules! my_struct_builder {
() => {};
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
let placement = analyzer.suggest_macro_placement("my_struct_builder");
assert_eq!(
placement,
MacroPlacement::WithType("MyStruct".to_string()),
"Expected WithType(MyStruct), got {:?}",
placement
);
}
#[test]
fn test_suggest_placement_standalone_for_unrelated_macro() {
let code = r#"
#[derive(Debug)]
struct Foo {
x: i32,
}
macro_rules! completely_unrelated_helper {
() => {};
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
assert_eq!(
analyzer.suggest_macro_placement("completely_unrelated_helper"),
MacroPlacement::Standalone
);
}
#[test]
fn test_all_custom_derives_deduplicated() {
let code = r#"
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct A { x: i32 }
#[derive(Clone, serde::Serialize)]
struct B { y: i32 }
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
let custom = analyzer.all_custom_derives();
let serialize_count = custom.iter().filter(|d| d.contains("Serialize")).count();
assert_eq!(
serialize_count, 1,
"serde::Serialize should appear only once"
);
}
#[test]
fn test_get_required_derives_unknown_type() {
let analyzer = MacroAnalyzer::new();
assert!(analyzer.get_required_derives("NonExistent").is_empty());
}
#[test]
fn test_usage_count_counted() {
let code = r#"
macro_rules! greet {
($name:expr) => { println!("Hello, {}!", $name) };
}
fn foo() {
greet!("world");
greet!("rust");
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
assert_eq!(analyzer.total_macro_count(), 1);
assert!(
analyzer.macro_rules[0].usage_count >= 1,
"Expected at least 1 usage, got {}",
analyzer.macro_rules[0].usage_count
);
}
#[test]
fn test_derives_on_enum() {
let code = r#"
#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
North,
South,
East,
West,
}
"#;
let file = syn::parse_file(code).expect("parse");
let mut analyzer = MacroAnalyzer::new();
analyzer.analyze_file(&file);
let info = analyzer
.derive_usage
.get("Direction")
.expect("Direction not found");
assert!(info.derives.iter().any(|d| d == "Debug"));
assert!(info.derives.iter().any(|d| d == "Copy"));
assert!(info.custom_derives.is_empty());
}
}