aboutsummaryrefslogtreecommitdiff
path: root/mingling_pathf
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_pathf')
-rw-r--r--mingling_pathf/README.md2
-rw-r--r--mingling_pathf/src/error.rs16
-rw-r--r--mingling_pathf/src/lib.rs1
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs26
-rw-r--r--mingling_pathf/src/patterns.rs4
-rw-r--r--mingling_pathf/src/patterns/basic_struct.rs13
-rw-r--r--mingling_pathf/src/patterns/chain.rs10
-rw-r--r--mingling_pathf/src/patterns/completion.rs10
-rw-r--r--mingling_pathf/src/patterns/dispatcher.rs23
-rw-r--r--mingling_pathf/src/patterns/dispatcher_clap.rs71
-rw-r--r--mingling_pathf/src/patterns/group.rs116
-rw-r--r--mingling_pathf/src/patterns/grouped_derive.rs89
-rw-r--r--mingling_pathf/src/patterns/groupped_derive.rs98
-rw-r--r--mingling_pathf/src/patterns/help.rs10
-rw-r--r--mingling_pathf/src/patterns/pack.rs15
-rw-r--r--mingling_pathf/src/patterns/renderer.rs10
-rw-r--r--mingling_pathf/src/type_mapping_builder.rs5
-rw-r--r--mingling_pathf/test/src/lib.rs4
-rw-r--r--mingling_pathf/test/src/test_files/test_grouped_derive.rs (renamed from mingling_pathf/test/src/test_files/test_groupped_derive.rs)16
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,
}