// Doc Not Optimize //! The `DispatcherPattern` matches invocations of the `dispatcher!` macro and //! extracts the generated type names from its arguments. It supports: //! - `Entry*` — the entry type (always generated) //! - `__Dispatcher*` — the hidden dispatcher struct (always generated) //! - `__internal_dispatcher_*` — the compile-time collected static (always generated) //! //! Supported forms: //! - Explicit: `dispatcher!("greet", EntryGreet)` //! - Implicit: `dispatcher!("greet")` — infers `EntryGreet` //! - With braces: `dispatcher! { ... }` //! //! This pattern is used to track dispatcher types for code generation or analysis. use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; /// Matches the `dispatcher!` macro, extracts: /// - `Entry*` — the entry type (always) /// - `__Dispatcher*` — the hidden dispatcher struct (always) /// - `__internal_dispatcher_*` — the compile-time collected static (always) #[derive(Default)] pub struct DispatcherPattern; impl DispatcherPattern { /// Creates a new `DispatcherPattern`. #[must_use] pub const fn new() -> Self { Self } } /// 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, "")); } 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(), )); } } } } _ => {} } } 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) -> Vec { let (cmd_name, entry_struct) = parse_dispatcher_args(tokens); let Some(cmd_name) = cmd_name else { return Vec::new(); }; let mut items = Vec::new(); // Entry type — always (derived from the command name in the implicit form) let entry = entry_struct.unwrap_or_else(|| format!("Entry{}", to_pascal_case(&cmd_name))); items.push(AnalyzeItem::local(module.to_string(), entry)); // Hidden dispatcher struct — always let hidden_name = format!("__Dispatcher{}", to_pascal_case(&cmd_name)); items.push(AnalyzeItem::local(module.to_string(), hidden_name)); // __internal_dispatcher_* — the compile-time collected static let internal_name = format!("__internal_dispatcher_{}", snake_case(&cmd_name)); items.push(AnalyzeItem::local(module.to_string(), internal_name)); items } /// Parses dispatcher arguments and returns (`command_name`, `entry_struct`). fn parse_dispatcher_args(tokens: &proc_macro2::TokenStream) -> (Option, Option) { let stream = tokens.to_string(); let Some(cmd_name) = extract_string_literal(&stream) else { return (None, None); }; // Explicit form: "name", EntryType — the entry is the first bare // identifier after the string literal. (The old `CMD => Entry` form is // no longer supported.) let after_lit = { let start = stream.find('"').unwrap_or_default(); &stream[start + cmd_name.len() + 2..] }; let entry_type = after_lit .split(|c: char| c.is_whitespace() || c == ',' || c == ')' || c == '}' || c == '=') .map(str::trim) .find(|s| !s.is_empty() && !s.starts_with('"')) .map(str::to_string); (Some(cmd_name), entry_type) } /// 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(); c.next().map_or_else(String::new, |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() }