diff options
Diffstat (limited to 'mingling_pathf')
19 files changed, 325 insertions, 214 deletions
diff --git a/mingling_pathf/README.md b/mingling_pathf/README.md index 89d1811..9706f49 100644 --- a/mingling_pathf/README.md +++ b/mingling_pathf/README.md @@ -7,7 +7,7 @@ ## Overview -`mingling_pathf` provides the `pathf` feature for Mingling. It automatically analyzes the full module paths of all Mingling types in a crate at build-time (types defined via `pack!`, `#[derive(Groupped)]`, `#[chain]`, `#[renderer]`, etc.), and generates a mapping from type names to module paths for consumption by `gen_program!()` at compile-time. +`mingling_pathf` provides the `pathf` feature for Mingling. It automatically analyzes the full module paths of all Mingling types in a crate at build-time (types defined via `pack!`, `#[derive(Grouped)]`, `#[chain]`, `#[renderer]`, etc.), and generates a mapping from type names to module paths for consumption by `gen_program!()` at compile-time. When enabled, `gen_program!()` uses full module paths for type references in the generated dispatch code (e.g., `downcast::<myapp::sub::ResultMyName>()`), eliminating the need to `use` all types in the module where `gen_program!()` is called. This allows for a more flexible module organization without the constraint of centralized `use` statements. diff --git a/mingling_pathf/src/error.rs b/mingling_pathf/src/error.rs index 70a8f6e..d384901 100644 --- a/mingling_pathf/src/error.rs +++ b/mingling_pathf/src/error.rs @@ -22,7 +22,9 @@ pub enum MinglingPathfinderError { /// `parent` is the directory containing the file that declared the module. /// `module_name` is the name of the module that could not be found. ModuleNotFound { + /// The directory containing the file that declared the module. parent: PathBuf, + /// The name of the module that could not be found. module_name: String, }, @@ -30,7 +32,12 @@ pub enum MinglingPathfinderError { /// /// `file` is the file containing the invalid attribute. /// `path_attr` is the value of the `#[path]` attribute. - PathPointsOutside { file: PathBuf, path_attr: String }, + PathPointsOutside { + /// The file containing the invalid `#[path]` attribute. + file: PathBuf, + /// The value of the `#[path]` attribute that points outside the project. + path_attr: String, + }, /// No entry point file (`main.rs`, `lib.rs`, or any file under `bin/`) was found. NoEntryPointFound, @@ -39,7 +46,12 @@ pub enum MinglingPathfinderError { /// /// `path` is the file that failed to parse. /// `message` contains details from the parser. - SynError { path: PathBuf, message: String }, + SynError { + /// The file that failed to parse. + path: PathBuf, + /// Details from the parser about the parse failure. + message: String, + }, } impl fmt::Display for MinglingPathfinderError { diff --git a/mingling_pathf/src/lib.rs b/mingling_pathf/src/lib.rs index 557ae45..69ff3ac 100644 --- a/mingling_pathf/src/lib.rs +++ b/mingling_pathf/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::needless_doctest_main)] #![doc = include_str!("../README.md")] +#![deny(missing_docs)] pub mod config; pub mod error; diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index 3765971..5b25c15 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -31,7 +31,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer { analyzer.add_pattern(BasicStructPattern); analyzer.add_pattern(PackPattern); analyzer.add_pattern(GroupPattern); - analyzer.add_pattern(GrouppedDerivePattern); + analyzer.add_pattern(GroupedDerivePattern); analyzer.add_pattern(ChainPattern); analyzer.add_pattern(RendererPattern); analyzer.add_pattern(HelpPattern); @@ -48,6 +48,28 @@ pub struct AnalyzeItem { pub module: String, /// The name of the item itself, e.g. `"HashMap"`, `"AnalyzeResult"`, etc. pub item_name: String, + /// Whether the item is from an external crate (resolved via `use`), bypassing the file's own module path. + pub is_foreign: bool, +} + +impl AnalyzeItem { + /// Creates a local `AnalyzeItem` (not foreign, will be prefixed with the file's module path). + pub fn local(module: String, item_name: String) -> Self { + Self { + module, + item_name, + is_foreign: false, + } + } + + /// Creates a foreign `AnalyzeItem` (resolved via `use`, the `module` field is the full import path). + pub fn foreign(module: String, item_name: String) -> Self { + Self { + module, + item_name, + is_foreign: true, + } + } } /// Collection of analysis results @@ -100,10 +122,12 @@ pub struct PatternAnalyzer { } impl PatternAnalyzer { + /// Creates a new empty `PatternAnalyzer`. pub fn new() -> Self { Self::default() } + /// Registers a new pattern for analysis. pub fn add_pattern(&mut self, pattern: impl AnalyzePattern + 'static) { self.patterns.push(Box::new(pattern)); } diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs index 9801e9b..9845b73 100644 --- a/mingling_pathf/src/patterns.rs +++ b/mingling_pathf/src/patterns.rs @@ -6,7 +6,7 @@ pub use completion::*; pub use dispatcher::*; pub use dispatcher_clap::*; pub use group::*; -pub use groupped_derive::*; +pub use grouped_derive::*; pub use help::*; pub use pack::*; pub use renderer::*; @@ -17,7 +17,7 @@ mod completion; mod dispatcher; mod dispatcher_clap; mod group; -mod groupped_derive; +mod grouped_derive; mod help; mod pack; mod renderer; diff --git a/mingling_pathf/src/patterns/basic_struct.rs b/mingling_pathf/src/patterns/basic_struct.rs index 09e8e70..9218d65 100644 --- a/mingling_pathf/src/patterns/basic_struct.rs +++ b/mingling_pathf/src/patterns/basic_struct.rs @@ -28,20 +28,17 @@ impl AnalyzePattern for BasicStructPattern { match item { // Root-level struct Item::Struct(s) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: s.ident.to_string(), - }); + items.push(AnalyzeItem::local(String::new(), s.ident.to_string())); } // Struct within inline modules Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { for n in nested { if let syn::Item::Struct(s) = n { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: s.ident.to_string(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + s.ident.to_string(), + )); } } } diff --git a/mingling_pathf/src/patterns/chain.rs b/mingling_pathf/src/patterns/chain.rs index 6393440..2e95db2 100644 --- a/mingling_pathf/src/patterns/chain.rs +++ b/mingling_pathf/src/patterns/chain.rs @@ -18,7 +18,7 @@ pub struct ChainPattern; impl AnalyzePattern for ChainPattern { fn contains(&self, content: &str) -> bool { - content.contains("chain]") + content.contains("[chain") || content.contains("chain]") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -44,10 +44,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "chain") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/patterns/completion.rs b/mingling_pathf/src/patterns/completion.rs index 5427b93..cff62e0 100644 --- a/mingling_pathf/src/patterns/completion.rs +++ b/mingling_pathf/src/patterns/completion.rs @@ -13,7 +13,7 @@ pub struct CompletionPattern; impl AnalyzePattern for CompletionPattern { fn contains(&self, content: &str) -> bool { - content.contains("completion(") + content.contains("completion(") || content.contains("[completion") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -37,10 +37,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "completion") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs index 6796a2c..cedad9f 100644 --- a/mingling_pathf/src/patterns/dispatcher.rs +++ b/mingling_pathf/src/patterns/dispatcher.rs @@ -20,10 +20,18 @@ use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; /// - `CMD*` — the dispatcher struct (always) /// - `__internal_dispatcher_*` — dispatch tree static (when `use_dispatch_tree` is true) pub struct DispatcherPattern { + /// Whether the dispatcher generates a dispatch tree static (`__internal_dispatcher_*`). pub use_dispatch_tree: bool, } impl DispatcherPattern { + /// Creates a new `DispatcherPattern`. + /// + /// # Arguments + /// + /// * `use_dispatch_tree` — when `true`, the generated dispatcher also produces a + /// `__internal_dispatcher_*` static dispatch tree item. Set this based on whether + /// your macro invocation includes the `use_dispatch_tree` configuration. pub fn new(use_dispatch_tree: bool) -> Self { Self { use_dispatch_tree } } @@ -103,27 +111,18 @@ fn extract_all_types( // Entry type — always if let Some(ref entry) = entry_struct { - items.push(AnalyzeItem { - module: module.to_string(), - item_name: entry.clone(), - }); + items.push(AnalyzeItem::local(module.to_string(), entry.clone())); } // CMD type — always if let Some(ref cmd) = cmd_struct { - items.push(AnalyzeItem { - module: module.to_string(), - item_name: cmd.clone(), - }); + items.push(AnalyzeItem::local(module.to_string(), 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.push(AnalyzeItem::local(module.to_string(), internal_name)); } items diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs index 1a86ad5..25a7093 100644 --- a/mingling_pathf/src/patterns/dispatcher_clap.rs +++ b/mingling_pathf/src/patterns/dispatcher_clap.rs @@ -29,10 +29,16 @@ use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; /// - `#[dispatcher_clap("greet", CMDGreet, help = true)] struct EntryGreet { ... }` /// - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet, help = true)] struct EntryGreet { ... }` pub struct DispatcherClapPattern { + /// Whether to include the `__internal_dispatcher_*` dispatch tree static in the analysis. pub use_dispatch_tree: bool, } impl DispatcherClapPattern { + /// Creates a new `DispatcherClapPattern` with the given configuration. + /// + /// # Parameters + /// - `use_dispatch_tree`: When `true`, enables analysis of the `__internal_dispatcher_*` + /// static dispatch tree for each matched command. pub fn new(use_dispatch_tree: bool) -> Self { Self { use_dispatch_tree } } @@ -55,10 +61,7 @@ impl AnalyzePattern for DispatcherClapPattern { 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: entry_name.clone(), - }); + items.push(AnalyzeItem::local(String::new(), entry_name.clone())); // Parse the attribute to extract CMD, error, and help info if let Some(attr) = s.attrs.iter().find(|a| { @@ -73,18 +76,12 @@ impl AnalyzePattern for DispatcherClapPattern { // CMD type — always if let Some(ref cmd) = parsed.cmd_type { - items.push(AnalyzeItem { - module: String::new(), - item_name: cmd.clone(), - }); + items.push(AnalyzeItem::local(String::new(), 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(), - }); + items.push(AnalyzeItem::local(String::new(), err.clone())); } // Help internal struct — if help = true @@ -94,10 +91,7 @@ impl AnalyzePattern for DispatcherClapPattern { 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, - }); + items.push(AnalyzeItem::local(String::new(), help_struct)); } // __internal_dispatcher_* — when configured @@ -108,10 +102,7 @@ impl AnalyzePattern for DispatcherClapPattern { "__internal_dispatcher_{}", just_fmt::snake_case!(cmd_name) ); - items.push(AnalyzeItem { - module: String::new(), - item_name: internal_name, - }); + items.push(AnalyzeItem::local(String::new(), internal_name)); } } } @@ -122,10 +113,10 @@ impl AnalyzePattern for DispatcherClapPattern { && 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(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + entry_name.clone(), + )); if let Some(attr) = s.attrs.iter().find(|a| { a.path() @@ -139,17 +130,17 @@ impl AnalyzePattern for DispatcherClapPattern { 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(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + cmd.clone(), + )); } if let Some(ref err) = parsed.error_type { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: err.clone(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + err.clone(), + )); } // Help internal struct — same naming rule as root level @@ -162,10 +153,10 @@ impl AnalyzePattern for DispatcherClapPattern { "__internal_help_{}", just_fmt::snake_case!(&help_fn) ); - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: help_struct, - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + help_struct, + )); } // __internal_dispatcher_* — when configured @@ -176,10 +167,10 @@ impl AnalyzePattern for DispatcherClapPattern { "__internal_dispatcher_{}", just_fmt::snake_case!(cmd_name) ); - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: internal_name, - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + internal_name, + )); } } } diff --git a/mingling_pathf/src/patterns/group.rs b/mingling_pathf/src/patterns/group.rs index 0e4b50d..0f9ecff 100644 --- a/mingling_pathf/src/patterns/group.rs +++ b/mingling_pathf/src/patterns/group.rs @@ -2,7 +2,10 @@ //! extracts the type name or alias defined within them. //! This is used to track type groups for code generation or analysis. +use std::collections::HashMap; + use syn::Item; +use syn::UseTree; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; @@ -25,6 +28,9 @@ impl AnalyzePattern for GroupPattern { return Vec::new(); }; + // Collect `use` imports at the file level + let imports = collect_use_imports(&syntax.items); + let mut items = Vec::new(); for item in &syntax.items { @@ -37,15 +43,15 @@ impl AnalyzePattern for GroupPattern { if macro_name != "group" && macro_name != "group_structural" { continue; } - if let Some(name) = extract_group_name(&m.mac.tokens) { - items.push(AnalyzeItem { - module: String::new(), - item_name: name, - }); + if let Some(analyze_item) = extract_group_item(&m.mac.tokens, &imports, "") { + items.push(analyze_item); } } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { + // Collect `use` imports inside this module + let inner_imports = collect_use_imports(nested); + for n in nested { if let Item::Macro(m) = n { let Some(last) = m.mac.path.segments.last() else { @@ -55,11 +61,12 @@ impl AnalyzePattern for GroupPattern { if macro_name != "group" && macro_name != "group_structural" { continue; } - if let Some(name) = extract_group_name(&m.mac.tokens) { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: name, - }); + if let Some(analyze_item) = extract_group_item( + &m.mac.tokens, + &inner_imports, + &item_mod.ident.to_string(), + ) { + items.push(analyze_item); } } } @@ -73,6 +80,95 @@ impl AnalyzePattern for GroupPattern { } } +/// Extract an `AnalyzeItem` from the macro invocation tokens. +/// +/// If the name matches a `use` import, resolves to the full foreign path. +/// For the `Alias = path::Type` form, returns a local item (the alias exists in-crate). +fn extract_group_item( + tokens: &proc_macro2::TokenStream, + imports: &HashMap<String, (String, String)>, + current_mod: &str, +) -> Option<AnalyzeItem> { + let name = extract_group_name(tokens)?; + let is_aliased = has_equals_sign(tokens); + + if is_aliased { + // `group!(Alias = path::Type)` — alias lives in-crate + Some(AnalyzeItem::local(current_mod.to_string(), name)) + } else if let Some((module, _)) = imports.get(&name) { + // `group!(TypeName)` where TypeName is imported via `use` — foreign + Some(AnalyzeItem::foreign(module.clone(), name)) + } else { + // `group!(LocalType)` — local type + Some(AnalyzeItem::local(current_mod.to_string(), name)) + } +} + +/// Collect `use` imports from a list of top-level items. +/// +/// Returns a map of `short_name → (module_path, short_name)`. +/// e.g. `use cargo_metadata::CompilerMessage;` → `"CompilerMessage" → ("cargo_metadata", "CompilerMessage")` +fn collect_use_imports(items: &[syn::Item]) -> HashMap<String, (String, String)> { + let mut map = HashMap::new(); + for item in items { + if let Item::Use(use_item) = item { + // Only handle non-`pub use` (regular imports) + collect_from_use_tree(&use_item.tree, "", &mut map); + } + } + map +} + +/// Recursively traverse a `UseTree` and collect named imports. +fn collect_from_use_tree( + tree: &UseTree, + prefix: &str, + map: &mut HashMap<String, (String, String)>, +) { + match tree { + UseTree::Name(name) => { + let module = prefix.to_string(); + let alias = name.ident.to_string(); + map.entry(alias).or_insert((module, name.ident.to_string())); + } + UseTree::Path(use_path) => { + let new_prefix = if prefix.is_empty() { + use_path.ident.to_string() + } else { + format!("{}::{}", prefix, use_path.ident) + }; + collect_from_use_tree(&use_path.tree, &new_prefix, map); + } + UseTree::Rename(rename) => { + let module = prefix.to_string(); + let alias = rename.ident.to_string(); + map.entry(alias) + .or_insert((module, rename.ident.to_string())); + } + UseTree::Glob(_) => { + // `use path::*;` — skip glob imports + } + UseTree::Group(group) => { + for item in &group.items { + collect_from_use_tree(item, prefix, map); + } + } + } +} + +/// Check whether the macro tokens contain `=`, indicating aliased form. +fn has_equals_sign(tokens: &proc_macro2::TokenStream) -> bool { + let stream = tokens.clone(); + for token in stream { + if let proc_macro2::TokenTree::Punct(p) = token + && p.as_char() == '=' + { + return true; + } + } + false +} + /// Extract the alias / type name from the arguments of `group!`. /// /// - `group!(ParseIntError)` → `ParseIntError` diff --git a/mingling_pathf/src/patterns/grouped_derive.rs b/mingling_pathf/src/patterns/grouped_derive.rs new file mode 100644 index 0000000..56a87f9 --- /dev/null +++ b/mingling_pathf/src/patterns/grouped_derive.rs @@ -0,0 +1,89 @@ +//! The `GroupedDerivePattern` matches structs, enums, and unions annotated with +//! `#[derive(Grouped)]` or `#[derive(GroupedSerialize)]` (or any combination +//! with other derives). It also recurses into `mod` items to find nested types. +//! This is used to track grouped items for code generation or analysis. + +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Matches `#[derive(Grouped)]` and `#[derive(GroupedSerialize)]`. +/// +/// Covers the forms: +/// - `#[derive(Grouped)] struct T { ... }` +/// - `#[derive(Grouped, Serialize, ...)] struct T { ... }` +/// - `#[derive(GroupedSerialize)] struct T { ... }` +pub struct GroupedDerivePattern; + +impl AnalyzePattern for GroupedDerivePattern { + fn contains(&self, content: &str) -> bool { + content.contains("Grouped") + } + + 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::Struct(s) if has_grouped_derive(&s.attrs) => { + items.push(AnalyzeItem::local(String::new(), s.ident.to_string())); + } + Item::Enum(e) if has_grouped_derive(&e.attrs) => { + items.push(AnalyzeItem::local(String::new(), e.ident.to_string())); + } + Item::Union(u) if has_grouped_derive(&u.attrs) => { + items.push(AnalyzeItem::local(String::new(), u.ident.to_string())); + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + for n in nested { + match n { + Item::Struct(s) if has_grouped_derive(&s.attrs) => { + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + s.ident.to_string(), + )); + } + Item::Enum(e) if has_grouped_derive(&e.attrs) => { + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + e.ident.to_string(), + )); + } + _ => {} + } + } + } + } + _ => {} + } + } + + items + } +} + +fn has_grouped_derive(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|attr| { + if attr.path().is_ident("derive") { + // Correctly parse comma-separated paths in #[derive(Grouped, 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 == "Grouped" || name == "GroupedSerialize" + })) + }) + .unwrap_or(false) + } else { + false + } + }) +} diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/groupped_derive.rs deleted file mode 100644 index 91daaef..0000000 --- a/mingling_pathf/src/patterns/groupped_derive.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! The `GrouppedDerivePattern` matches structs, enums, and unions annotated with -//! `#[derive(Groupped)]` or `#[derive(GrouppedSerialize)]` (or any combination -//! with other derives). It also recurses into `mod` items to find nested types. -//! This is used to track grouped items for code generation or analysis. - -use syn::Item; - -use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; - -/// Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]`. -/// -/// Covers the forms: -/// - `#[derive(Groupped)] struct T { ... }` -/// - `#[derive(Groupped, Serialize, ...)] struct T { ... }` -/// - `#[derive(GrouppedSerialize)] struct T { ... }` -pub struct GrouppedDerivePattern; - -impl AnalyzePattern for GrouppedDerivePattern { - fn contains(&self, content: &str) -> bool { - content.contains("Groupped") - } - - 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::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 { - match n { - Item::Struct(s) if has_groupped_derive(&s.attrs) => { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: s.ident.to_string(), - }); - } - Item::Enum(e) if has_groupped_derive(&e.attrs) => { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: e.ident.to_string(), - }); - } - _ => {} - } - } - } - } - _ => {} - } - } - - items - } -} - -fn has_groupped_derive(attrs: &[syn::Attribute]) -> bool { - attrs.iter().any(|attr| { - if attr.path().is_ident("derive") { - // 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/src/patterns/help.rs b/mingling_pathf/src/patterns/help.rs index 628f4ac..85793c3 100644 --- a/mingling_pathf/src/patterns/help.rs +++ b/mingling_pathf/src/patterns/help.rs @@ -13,7 +13,7 @@ pub struct HelpPattern; impl AnalyzePattern for HelpPattern { fn contains(&self, content: &str) -> bool { - content.contains("help]") + content.contains("[help") || content.contains("help]") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -37,10 +37,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "help") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/patterns/pack.rs b/mingling_pathf/src/patterns/pack.rs index c80fb65..76597ba 100644 --- a/mingling_pathf/src/patterns/pack.rs +++ b/mingling_pathf/src/patterns/pack.rs @@ -18,7 +18,10 @@ pub struct PackPattern; impl AnalyzePattern for PackPattern { fn contains(&self, content: &str) -> bool { - content.contains("pack!") || content.contains("pack_err!") + content.contains("pack!") + || content.contains("pack_err!") + || content.contains("pack_structural!") + || content.contains("pack_err_structural!") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -33,10 +36,7 @@ impl AnalyzePattern for PackPattern { // Top-level macro calls Item::Macro(m) => { if let Some(name) = try_extract_pack_name(m) { - items.push(AnalyzeItem { - module: String::new(), - item_name: name, - }); + items.push(AnalyzeItem::local(String::new(), name)); } } // Macro calls inside inline modules @@ -46,10 +46,7 @@ impl AnalyzePattern for PackPattern { if let Item::Macro(m) = n && let Some(name) = try_extract_pack_name(m) { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: name, - }); + items.push(AnalyzeItem::local(item_mod.ident.to_string(), name)); } } } diff --git a/mingling_pathf/src/patterns/renderer.rs b/mingling_pathf/src/patterns/renderer.rs index c2e9ca9..153e603 100644 --- a/mingling_pathf/src/patterns/renderer.rs +++ b/mingling_pathf/src/patterns/renderer.rs @@ -13,7 +13,7 @@ pub struct RendererPattern; impl AnalyzePattern for RendererPattern { fn contains(&self, content: &str) -> bool { - content.contains("renderer]") + content.contains("[renderer") || content.contains("renderer]") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -37,10 +37,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "renderer") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/type_mapping_builder.rs b/mingling_pathf/src/type_mapping_builder.rs index 0965b47..f6f57a5 100644 --- a/mingling_pathf/src/type_mapping_builder.rs +++ b/mingling_pathf/src/type_mapping_builder.rs @@ -39,7 +39,10 @@ pub fn analyze_and_build_type_mapping_for( }; for ai in analyze_items { - let full_path = if ai.module.is_empty() { + let full_path = if ai.is_foreign { + // Foreign item — use its own module path as-is + format!("{}::{}", ai.module, ai.item_name) + } else if ai.module.is_empty() { format!("{}::{}", module_path, ai.item_name) } else { format!("{}::{}::{}", module_path, ai.module, ai.item_name) diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs index 824cbbf..7e7cbd5 100644 --- a/mingling_pathf/test/src/lib.rs +++ b/mingling_pathf/test/src/lib.rs @@ -222,11 +222,11 @@ fn test_group_analyze() { } #[test] -fn test_groupped_derive_analyze() { +fn test_grouped_derive_analyze() { let analyzer = mingling_pathf::pattern_analyzer::init(); let file = current_dir() .unwrap() - .join("src/test_files/test_groupped_derive.rs"); + .join("src/test_files/test_grouped_derive.rs"); let r = analyzer.analyze_file(file).unwrap(); let required: Vec<&str> = vec![ diff --git a/mingling_pathf/test/src/test_files/test_groupped_derive.rs b/mingling_pathf/test/src/test_files/test_grouped_derive.rs index 913587c..20055e9 100644 --- a/mingling_pathf/test/src/test_files/test_groupped_derive.rs +++ b/mingling_pathf/test/src/test_files/test_grouped_derive.rs @@ -1,42 +1,42 @@ -#[derive(Groupped)] +#[derive(Grouped)] struct Derived1 { value: String, } -#[derive(Groupped, Debug, Clone)] +#[derive(Grouped, Debug, Clone)] struct Derived2 { value: i32, } -#[derive(GrouppedSerialize)] +#[derive(GroupedSerialize)] struct Derived3 { value: bool, } -#[derive(Groupped)] +#[derive(Grouped)] enum EnumDerived1 { A, B, } -#[derive(GrouppedSerialize)] +#[derive(GroupedSerialize)] enum EnumDerived2 { X(String), Y(i32), } pub mod sub { - #[derive(Groupped)] + #[derive(Grouped)] struct Derived1 { value: String, } - #[derive(GrouppedSerialize)] + #[derive(GroupedSerialize)] struct Derived3 { value: bool, } - #[derive(Groupped)] + #[derive(Grouped)] enum EnumDerived1 { A, } |
