diff options
Diffstat (limited to 'mingling_pathf')
| -rw-r--r-- | mingling_pathf/Cargo.toml | 4 | ||||
| -rw-r--r-- | mingling_pathf/src/pattern_analyzer.rs | 2 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/dispatcher_clap.rs | 234 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/groupped_derive.rs | 53 | ||||
| -rw-r--r-- | mingling_pathf/test/Cargo.lock | 1 | ||||
| -rw-r--r-- | mingling_pathf/test/src/lib.rs | 59 | ||||
| -rw-r--r-- | mingling_pathf/test/src/test_files/test_dispatcher_clap.rs | 40 | ||||
| -rw-r--r-- | mingling_pathf/test/src/test_files/test_groupped_derive.rs | 17 |
8 files changed, 367 insertions, 43 deletions
diff --git a/mingling_pathf/Cargo.toml b/mingling_pathf/Cargo.toml index 9619adc..0d4e37a 100644 --- a/mingling_pathf/Cargo.toml +++ b/mingling_pathf/Cargo.toml @@ -2,9 +2,13 @@ name = "mingling_pathf" version.workspace = true edition.workspace = true +authors = ["Weicao-CatilGrass"] license.workspace = true repository.workspace = true +readme = "README.md" +description = "A library for automatically finding internal types generated by Mingling" [dependencies] syn.workspace = true proc-macro2.workspace = true +just_fmt.workspace = true diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index bfc2dc3..c4b1971 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -23,7 +23,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer { analyzer.add_pattern(HelpPattern); analyzer.add_pattern(CompletionPattern); analyzer.add_pattern(DispatcherPattern::new(config.use_dispatch_tree)); - analyzer.add_pattern(DispatcherClapPattern); + analyzer.add_pattern(DispatcherClapPattern::new(config.use_dispatch_tree)); analyzer } diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs index 398b269..2e1ec6c 100644 --- a/mingling_pathf/src/patterns/dispatcher_clap.rs +++ b/mingling_pathf/src/patterns/dispatcher_clap.rs @@ -2,12 +2,27 @@ use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; -/// Match structs annotated with `#[dispatcher_clap]`, extracting the entry type name (i.e., the struct name). +/// Match structs annotated with `#[dispatcher_clap(...)]`, extracting: +/// - The entry type (struct name, always) +/// - The dispatcher struct (`CMD*`, always) +/// - The error type, if `error = ErrorType` is specified +/// - The help internal struct, if `help = true` is specified +/// - `__internal_dispatcher_*` — dispatch tree static (when `use_dispatch_tree` is true) /// -/// Covers the following forms: -/// - `#[dispatcher_clap] struct EntryGreet { ... }` -/// - `#[dispatcher_clap] #[command(...)] struct EntryGreet { ... }` -pub struct DispatcherClapPattern; +/// Covers forms: +/// - `#[dispatcher_clap("greet", CMDGreet)] struct EntryGreet { ... }` +/// - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet)] struct EntryGreet { ... }` +/// - `#[dispatcher_clap("greet", CMDGreet, help = true)] struct EntryGreet { ... }` +/// - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet, help = true)] struct EntryGreet { ... }` +pub struct DispatcherClapPattern { + pub use_dispatch_tree: bool, +} + +impl DispatcherClapPattern { + pub fn new(use_dispatch_tree: bool) -> Self { + Self { use_dispatch_tree } + } +} impl AnalyzePattern for DispatcherClapPattern { fn contains(&self, content: &str) -> bool { @@ -24,21 +39,136 @@ impl AnalyzePattern for DispatcherClapPattern { for item in &syntax.items { match item { Item::Struct(s) if has_attr(&s.attrs, "dispatcher_clap") => { + // Entry type (struct name) — always + let entry_name = s.ident.to_string(); items.push(AnalyzeItem { module: String::new(), - item_name: s.ident.to_string(), + item_name: entry_name.clone(), }); + + // Parse the attribute to extract CMD, error, and help info + if let Some(attr) = s.attrs.iter().find(|a| { + a.path() + .segments + .last() + .is_some_and(|seg| seg.ident == "dispatcher_clap") + }) { + let args = attr.meta.require_list().ok(); + let args_str = args.map(|l| l.tokens.to_string()).unwrap_or_default(); + let parsed = parse_dispatcher_clap_args(&args_str); + + // CMD type — always + if let Some(ref cmd) = parsed.cmd_type { + items.push(AnalyzeItem { + module: String::new(), + item_name: cmd.clone(), + }); + } + + // Error type — if error = TypeName + if let Some(ref err) = parsed.error_type { + items.push(AnalyzeItem { + module: String::new(), + item_name: err.clone(), + }); + } + + // Help internal struct — if help = true + if parsed.help_enabled + && let Some(ref cmd) = parsed.cmd_type + { + let help_fn = format!("__{}_help", just_fmt::snake_case!(cmd)); + let help_struct = + format!("__internal_help_{}", just_fmt::snake_case!(&help_fn)); + items.push(AnalyzeItem { + module: String::new(), + item_name: help_struct, + }); + } + + // __internal_dispatcher_* — when configured + if self.use_dispatch_tree + && let Some(ref cmd_name) = parsed.cmd_name + { + let internal_name = format!( + "__internal_dispatcher_{}", + just_fmt::snake_case!(cmd_name) + ); + items.push(AnalyzeItem { + module: String::new(), + item_name: internal_name, + }); + } + } } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { for n in nested { if let Item::Struct(s) = n - && has_attr(&s.attrs, "dispatcher_clap") { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: s.ident.to_string(), - }); + && has_attr(&s.attrs, "dispatcher_clap") + { + let entry_name = s.ident.to_string(); + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: entry_name.clone(), + }); + + if let Some(attr) = s.attrs.iter().find(|a| { + a.path() + .segments + .last() + .is_some_and(|seg| seg.ident == "dispatcher_clap") + }) { + let args = attr.meta.require_list().ok(); + let args_str = + args.map(|l| l.tokens.to_string()).unwrap_or_default(); + let parsed = parse_dispatcher_clap_args(&args_str); + + if let Some(ref cmd) = parsed.cmd_type { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: cmd.clone(), + }); + } + + if let Some(ref err) = parsed.error_type { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: err.clone(), + }); + } + + // Help internal struct — same naming rule as root level + if parsed.help_enabled + && let Some(ref cmd) = parsed.cmd_type + { + let help_fn = + format!("__{}_help", just_fmt::snake_case!(cmd)); + let help_struct = format!( + "__internal_help_{}", + just_fmt::snake_case!(&help_fn) + ); + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: help_struct, + }); + } + + // __internal_dispatcher_* — when configured + if self.use_dispatch_tree + && let Some(ref cmd_name) = parsed.cmd_name + { + let internal_name = format!( + "__internal_dispatcher_{}", + just_fmt::snake_case!(cmd_name) + ); + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: internal_name, + }); + } } + } } } } @@ -50,11 +180,81 @@ impl AnalyzePattern for DispatcherClapPattern { } } +struct ParsedClapArgs { + cmd_name: Option<String>, + cmd_type: Option<String>, + error_type: Option<String>, + help_enabled: bool, +} + +/// Parse `#[dispatcher_clap("cmd", CMDType, error = ErrorType, help = true)]` arguments. +fn parse_dispatcher_clap_args(args: &str) -> ParsedClapArgs { + let mut cmd_name = None; + let mut cmd_type = None; + let mut error_type = None; + let mut help_enabled = false; + + let args = args.trim(); + + // Extract the first quoted string (the command name) + let after_cmd = if let Some(start) = args.find('"') { + let after_open = &args[start + 1..]; + if let Some(end) = after_open.find('"') { + cmd_name = Some(after_open[..end].to_string()); + after_open[end + 1..].trim() + } else { + args + } + } else { + args + }; + + // Split by commas and parse each part + for part in after_cmd.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + + // Skip the command name (first quoted string should already be removed) + if part.starts_with('"') { + continue; + } + + // Check for key = value + if let Some(eq_idx) = part.find('=') { + let key = part[..eq_idx].trim(); + let value = part[eq_idx + 1..].trim(); + let value = value.trim_end_matches([')', ']']).trim(); + + match key { + "error" => { + error_type = Some(value.to_string()); + } + "help" => { + help_enabled = value == "true"; + } + _ => {} + } + } else { + // Bare ident — the CMD type + let clean = part.trim_end_matches([')', ']']).trim(); + if !clean.is_empty() && cmd_type.is_none() { + cmd_type = Some(clean.to_string()); + } + } + } + + ParsedClapArgs { + cmd_name, + cmd_type, + error_type, + help_enabled, + } +} + fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { - attrs.iter().any(|a| { - a.path() - .segments - .last() - .is_some_and(|s| s.ident == name) - }) + attrs + .iter() + .any(|a| a.path().segments.last().is_some_and(|s| s.ident == name)) } diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/groupped_derive.rs index 44e7731..8491121 100644 --- a/mingling_pathf/src/patterns/groupped_derive.rs +++ b/mingling_pathf/src/patterns/groupped_derive.rs @@ -24,27 +24,24 @@ impl AnalyzePattern for GrouppedDerivePattern { for item in &syntax.items { match item { - Item::Struct(s) - if has_groupped_derive(&s.attrs) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: s.ident.to_string(), - }); - } - Item::Enum(e) - if has_groupped_derive(&e.attrs) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: e.ident.to_string(), - }); - } - Item::Union(u) - if has_groupped_derive(&u.attrs) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: u.ident.to_string(), - }); - } + Item::Struct(s) if has_groupped_derive(&s.attrs) => { + items.push(AnalyzeItem { + module: String::new(), + item_name: s.ident.to_string(), + }); + } + Item::Enum(e) if has_groupped_derive(&e.attrs) => { + items.push(AnalyzeItem { + module: String::new(), + item_name: e.ident.to_string(), + }); + } + Item::Union(u) if has_groupped_derive(&u.attrs) => { + items.push(AnalyzeItem { + module: String::new(), + item_name: u.ident.to_string(), + }); + } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { for n in nested { @@ -77,12 +74,18 @@ impl AnalyzePattern for GrouppedDerivePattern { fn has_groupped_derive(attrs: &[syn::Attribute]) -> bool { attrs.iter().any(|attr| { if attr.path().is_ident("derive") { - attr.parse_args::<syn::MetaList>().ok().is_some_and(|meta| { - meta.path.segments.iter().any(|seg| { - let name = seg.ident.to_string(); + // Correctly parse comma-separated paths in #[derive(Groupped, Debug, ...)] + attr.parse_args_with(|input: syn::parse::ParseStream| { + let paths = + syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated( + input, + )?; + Ok(paths.iter().any(|p| { + let name = p.segments.last().unwrap().ident.to_string(); name == "Groupped" || name == "GrouppedSerialize" - }) + })) }) + .unwrap_or(false) } else { false } diff --git a/mingling_pathf/test/Cargo.lock b/mingling_pathf/test/Cargo.lock index 6c89ae3..e5fd23a 100644 --- a/mingling_pathf/test/Cargo.lock +++ b/mingling_pathf/test/Cargo.lock @@ -12,6 +12,7 @@ checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" name = "mingling_pathf" version = "0.2.0" dependencies = [ + "just_fmt", "proc-macro2", "syn", ] diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs index 51e19a6..824cbbf 100644 --- a/mingling_pathf/test/src/lib.rs +++ b/mingling_pathf/test/src/lib.rs @@ -233,8 +233,11 @@ fn test_groupped_derive_analyze() { "::Derived1", "::Derived2", "::Derived3", + "::EnumDerived1", + "::EnumDerived2", "::sub::Derived1", "::sub::Derived3", + "::sub::EnumDerived1", ]; assert_eq!(r.len(), required.len()); @@ -315,12 +318,41 @@ fn test_dispatcher_clap_analyze() { let r = analyzer.analyze_file(file).unwrap(); let required: Vec<&str> = vec![ + // Root: entry types (bare dispatcher_clap, no params) "::EntryClap1", "::EntryClap2", "::EntryClap3", "::EntryClap4", + // Root: with CMD type + "::EntryWithCmd", + "::CMDGreet", + // Root: with CMD + error + "::EntryWithError", + "::CMDDelete", + "::ErrorDelete", + // Root: with CMD + help + "::EntryWithHelp", + "::CMDHelp", + "::__internal_help_cmdhelp_help", + // Root: with CMD + error + help + "::EntryFull", + "::CMDFull", + "::ErrorFull", + "::__internal_help_cmdfull_help", + // Sub: entry types (bare dispatcher_clap) "::sub::EntryClap1", "::sub::EntryClap3", + // Sub: with CMD type + "::sub::EntryWithCmd", + "::sub::CMDGreet", + // Sub: with CMD + error + "::sub::EntryWithError", + "::sub::CMDDelete", + "::sub::ErrorDelete", + // Sub: with CMD + help + "::sub::EntryWithHelp", + "::sub::CMDHelp", + "::sub::__internal_help_cmdhelp_help", ]; assert_eq!(r.len(), required.len()); @@ -328,3 +360,30 @@ fn test_dispatcher_clap_analyze() { assert!(r.contains(*entry), "Result should contain: {}", entry); } } + +#[test] +fn test_dispatcher_clap_dispatch_tree() { + use mingling_pathf::config::PathfinderConfig; + use mingling_pathf::pattern_analyzer; + + let file = current_dir() + .unwrap() + .join("src/test_files/test_dispatcher_clap.rs"); + + // Without dispatch_tree: 26 items (same set as test_dispatcher_clap_analyze) + let r1 = pattern_analyzer::init().analyze_file(&file).unwrap(); + assert_eq!(r1.len(), 26); + + // With dispatch_tree: 26 + 4 __internal (root) + 3 __internal (sub, no "full") = 33 + let r2 = pattern_analyzer::init_with_config(PathfinderConfig::with_dispatch_tree()) + .analyze_file(&file) + .unwrap(); + assert_eq!(r2.len(), 33); + assert!(r2.contains("::__internal_dispatcher_greet")); + assert!(r2.contains("::__internal_dispatcher_delete")); + assert!(r2.contains("::__internal_dispatcher_helpcmd")); + assert!(r2.contains("::__internal_dispatcher_full")); + assert!(r2.contains("::sub::__internal_dispatcher_greet")); + assert!(r2.contains("::sub::__internal_dispatcher_delete")); + assert!(r2.contains("::sub::__internal_dispatcher_helpcmd")); +} diff --git a/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs index 0ba884d..33d86e0 100644 --- a/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs +++ b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs @@ -1,3 +1,4 @@ +// Basic: entry type only (no CMD type specified) #[mingling::macros::dispatcher_clap] struct EntryClap1 { name: String, @@ -20,6 +21,30 @@ pub struct EntryClap4 { value: i32, } +// With CMD type +#[dispatcher_clap("greet", CMDGreet)] +struct EntryWithCmd { + name: String, +} + +// With CMD + error +#[dispatcher_clap("delete", CMDDelete, error = ErrorDelete)] +struct EntryWithError { + id: u64, +} + +// With CMD + help +#[dispatcher_clap("helpcmd", CMDHelp, help = true)] +struct EntryWithHelp { + verbose: bool, +} + +// With CMD + error + help +#[dispatcher_clap("full", CMDFull, error = ErrorFull, help = true)] +struct EntryFull { + all: bool, +} + pub mod sub { #[mingling::macros::dispatcher_clap] struct EntryClap1 { @@ -30,4 +55,19 @@ pub mod sub { struct EntryClap3 { value: String, } + + #[dispatcher_clap("greet", CMDGreet)] + struct EntryWithCmd { + name: String, + } + + #[dispatcher_clap("delete", CMDDelete, error = ErrorDelete)] + struct EntryWithError { + id: u64, + } + + #[dispatcher_clap("helpcmd", CMDHelp, help = true)] + struct EntryWithHelp { + verbose: bool, + } } diff --git a/mingling_pathf/test/src/test_files/test_groupped_derive.rs b/mingling_pathf/test/src/test_files/test_groupped_derive.rs index f6c6fa9..913587c 100644 --- a/mingling_pathf/test/src/test_files/test_groupped_derive.rs +++ b/mingling_pathf/test/src/test_files/test_groupped_derive.rs @@ -13,6 +13,18 @@ struct Derived3 { value: bool, } +#[derive(Groupped)] +enum EnumDerived1 { + A, + B, +} + +#[derive(GrouppedSerialize)] +enum EnumDerived2 { + X(String), + Y(i32), +} + pub mod sub { #[derive(Groupped)] struct Derived1 { @@ -23,4 +35,9 @@ pub mod sub { struct Derived3 { value: bool, } + + #[derive(Groupped)] + enum EnumDerived1 { + A, + } } |
