use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; /// Matches the `dispatcher!` macro, extracts: /// - `Entry*` — the entry type (always) /// - `CMD*` — the dispatcher struct (always) /// - `__internal_dispatcher_*` — dispatch tree static (when `use_dispatch_tree` is true) pub struct DispatcherPattern { pub use_dispatch_tree: bool, } impl DispatcherPattern { pub fn new(use_dispatch_tree: bool) -> Self { Self { use_dispatch_tree } } } /// Supported forms: /// - `dispatcher!("greet", CMDGreet => EntryGreet)` — explicit /// - `dispatcher!("greet")` — implicit, infers names /// - `dispatcher! { ... }` — with braces impl AnalyzePattern for DispatcherPattern { fn contains(&self, content: &str) -> bool { content.contains("dispatcher!") } fn analyze(&self, content: &str) -> Vec { 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; } items.extend(extract_all_types(&m.mac.tokens, "", self.use_dispatch_tree)); } 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; } items.extend(extract_all_types( &m.mac.tokens, &item_mod.ident.to_string(), self.use_dispatch_tree, )); } } } } _ => {} } } items } } fn macro_simple_name(m: &syn::ItemMacro) -> String { m.mac .path .segments .last() .map(|s| s.ident.to_string()) .unwrap_or_default() } /// Extracts all types generated by a `dispatcher!` call. fn extract_all_types( tokens: &proc_macro2::TokenStream, module: &str, use_dispatch_tree: bool, ) -> Vec { let (cmd_name, cmd_struct, entry_struct) = parse_dispatcher_args(tokens); let cmd_name = match cmd_name { Some(n) => n, None => return Vec::new(), }; let mut items = Vec::new(); // Entry type — always if let Some(ref entry) = entry_struct { items.push(AnalyzeItem { module: module.to_string(), item_name: entry.clone(), }); } // CMD type — always if let Some(ref cmd) = cmd_struct { items.push(AnalyzeItem { module: module.to_string(), item_name: cmd.clone(), }); } // __internal_dispatcher_* — when configured if use_dispatch_tree { let internal_name = format!("__internal_dispatcher_{}", snake_case(&cmd_name)); items.push(AnalyzeItem { module: module.to_string(), item_name: internal_name, }); } items } /// Parses dispatcher arguments and returns (command_name, cmd_struct, entry_struct). fn parse_dispatcher_args( tokens: &proc_macro2::TokenStream, ) -> (Option, Option, Option) { let stream = tokens.to_string(); // Explicit form: "name", CMDType => EntryType if let Some(arrow_idx) = stream.find("=>") { // Extract command name let before_arrow = &stream[..arrow_idx]; let cmd_name = extract_string_literal(before_arrow); // Extract CMD type: the ident before `=>` let before_arrow_trimmed = before_arrow.trim(); let cmd_type = before_arrow_trimmed .split(|c: char| c.is_whitespace() || c == ',') .filter_map(|s| { let s = s.trim(); if s.starts_with('"') || s.is_empty() { None } else { Some(s.to_string()) } }) .next_back(); // Extract entry type: after `=>` let after_arrow = stream[arrow_idx + 2..].trim(); let entry_type = after_arrow .split(|c: char| c.is_whitespace() || c == ',' || c == ')' || c == '}') .next() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()); return (cmd_name, cmd_type, entry_type); } // Implicit form: "name" let cmd_name = match extract_string_literal(&stream) { Some(n) => n, None => return (None, None, None), }; let pascal = to_pascal_case(&cmd_name); ( Some(cmd_name), Some(format!("CMD{pascal}")), Some(format!("Entry{pascal}")), ) } /// Extracts the first string literal from a token string. fn extract_string_literal(s: &str) -> Option { let s = s.trim(); let start = s.find('"')?; let rest = &s[start + 1..]; let end = rest.find('"')?; Some(rest[..end].to_string()) } 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::() + c.as_str(), } }) .collect() } /// Simple snake_case conversion (replaces `.`, `-` with `_`). fn snake_case(s: &str) -> String { s.replace(['.', '-'], "_").to_lowercase() }