1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
use syn::Item;
use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
/// Matches the `dispatcher!` macro, extracts the entry type name.
///
/// Supported forms:
/// - `dispatcher!("greet", CMDGreet => EntryGreet)` — explicit
/// - `dispatcher!("greet")` — implicit, infers EntryType
/// - `dispatcher! { ... }` — with braces
pub struct DispatcherPattern;
impl AnalyzePattern for DispatcherPattern {
fn contains(&self, content: &str) -> bool {
content.contains("dispatcher!")
}
fn analyze(&self, content: &str) -> Vec<AnalyzeItem> {
let Ok(syntax) = syn::parse_file(content) else {
return Vec::new();
};
let mut items = Vec::new();
for item in &syntax.items {
match item {
Item::Macro(m) => {
let macro_name = macro_simple_name(m);
if macro_name != "dispatcher" {
continue;
}
if let Some(entry_name) = extract_dispatcher_entry(&m.mac.tokens) {
items.push(AnalyzeItem {
module: String::new(),
item_name: entry_name,
});
}
}
Item::Mod(item_mod) => {
if let Some((_, nested)) = &item_mod.content {
for n in nested {
if let Item::Macro(m) = n {
if macro_simple_name(m) != "dispatcher" {
continue;
}
if let Some(entry_name) = extract_dispatcher_entry(&m.mac.tokens) {
items.push(AnalyzeItem {
module: item_mod.ident.to_string(),
item_name: entry_name,
});
}
}
}
}
}
_ => {}
}
}
items
}
}
fn macro_simple_name(m: &syn::ItemMacro) -> String {
m.mac
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default()
}
/// Extracts the entry type name from the `dispatcher!` macro arguments.
///
/// Input examples:
/// - `"greet", CMDGreet => EntryGreet` → `EntryGreet`
/// - `"remote.add"` → `EntryRemoteAdd` (implicit inference)
fn extract_dispatcher_entry(tokens: &proc_macro2::TokenStream) -> Option<String> {
let stream = tokens.to_string();
// Explicit form: look for `=>`
if let Some(arrow_idx) = stream.find("=>") {
let after_arrow = stream[arrow_idx + 2..].trim();
let entry_name = after_arrow
.split(|c: char| c.is_whitespace() || c == ',' || c == ')' || c == '}')
.next()?;
if !entry_name.is_empty() {
return Some(entry_name.trim().to_string());
}
}
// Implicit form: infer from command name
let stream = stream.trim();
if let Some(start) = stream.find('"') {
let rest = &stream[start + 1..];
let cmd_name = rest.split('"').next()?;
let last_segment = cmd_name.split('.').next_back()?;
let entry = format!("Entry{}", to_pascal_case(last_segment));
Some(entry)
} else {
None
}
}
fn to_pascal_case(s: &str) -> String {
s.split(['-', '_', '.'])
.filter(|s| !s.is_empty())
.map(|s| {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect()
}
|