aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-28 09:06:08 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-28 09:06:08 +0800
commit748c14588cf1c31c8b8d60a9c94349c0173ef607 (patch)
tree4c09bfafd93b629a68f0f78902a33e8dd9ef18d1
parent50f2d767e2d07685e49fb7deae68d506ea11a79d (diff)
feat(pathf): add build-time type path resolution system
Add `mingling_pathf` sub-crate and `pathf` feature for automatic resolution of Mingling type module paths at build time. Scans source files, identifies macro invocations via pattern matchers, and generates mapping files consumed by `gen_program!()`.
-rw-r--r--CHANGELOG.md41
-rw-r--r--dev_tools/src/bin/test-examples.rs2
-rw-r--r--dev_tools/src/lib.rs2
-rw-r--r--mingling_core/src/builds.rs5
-rw-r--r--mingling_core/src/lib.rs9
-rw-r--r--mingling_macros/src/lib.rs10
-rw-r--r--mingling_pathf/README.md2
-rw-r--r--mingling_pathf/src/lib.rs4
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs28
-rw-r--r--mingling_pathf/src/patterns.rs21
-rw-r--r--mingling_pathf/src/patterns/chain.rs72
-rw-r--r--mingling_pathf/src/patterns/completion.rs65
-rw-r--r--mingling_pathf/src/patterns/dispatcher.rs116
-rw-r--r--mingling_pathf/src/patterns/dispatcher_clap.rs60
-rw-r--r--mingling_pathf/src/patterns/group.rs101
-rw-r--r--mingling_pathf/src/patterns/groupped_derive.rs93
-rw-r--r--mingling_pathf/src/patterns/help.rs65
-rw-r--r--mingling_pathf/src/patterns/pack.rs103
-rw-r--r--mingling_pathf/src/patterns/renderer.rs65
-rw-r--r--mingling_pathf/src/type_mapping_builder.rs101
-rw-r--r--mingling_pathf/test/src/.gitignore1
-rw-r--r--mingling_pathf/test/src/lib.rs235
-rw-r--r--mingling_pathf/test/src/test_files/test_chain.rs61
-rw-r--r--mingling_pathf/test/src/test_files/test_completion.rs41
-rw-r--r--mingling_pathf/test/src/test_files/test_dispatcher.rs17
-rw-r--r--mingling_pathf/test/src/test_files/test_dispatcher_clap.rs33
-rw-r--r--mingling_pathf/test/src/test_files/test_group.rs13
-rw-r--r--mingling_pathf/test/src/test_files/test_groupped_derive.rs26
-rw-r--r--mingling_pathf/test/src/test_files/test_help.rs33
-rw-r--r--mingling_pathf/test/src/test_files/test_pack.rs17
-rw-r--r--mingling_pathf/test/src/test_files/test_renderer.rs33
31 files changed, 1468 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9627967..0ec479d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -285,6 +285,47 @@ async fn greet(prev: HelloEntry, ec: &mut ResExitCode) -> Next {
}
```
+13. **[`pathf`]** Added the `mingling_pathf` sub-crate and the `pathf` feature for build-time type path resolution.
+
+The `pathf` (pathfinder) system enables automatic resolution of type module paths at build time. It scans source files, identifies Mingling macro invocations (`pack!`, `#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`, `dispatcher!`, `#[dispatcher_clap]`, `group!`, `#[derive(Groupped)]`, etc.), infers their module paths from the file system layout, and generates a mapping file consumed by `gen_program!()` at compile time.
+
+**Feature activation**: Enable the `pathf` feature on the `mingling` crate:
+
+```toml
+mingling = { version = "0.2", features = ["pathf"] }
+```
+
+**Usage** — Add a `build.rs` to your project:
+
+```rust
+// build.rs
+fn main() {
+ mingling::pathf::analyze_and_build_type_mapping().unwrap();
+}
+```
+
+The pathfinder system consists of:
+
+- **`mingling_pathf` sub-crate** — A standalone crate for build-time source analysis:
+ - `module_pathf::analyze()` — Scans the crate's source tree and infers module paths from the directory structure
+ - `pattern_analyzer::init()` — Creates a `PatternAnalyzer` registered with all supported Mingling patterns
+ - `analyze_and_build_type_mapping()` / `analyze_and_build_type_mapping_for()` — Convenience functions for build scripts
+ - **Pattern matchers** — Individual pattern implementations for each Mingling macro:
+ - `PackPattern` — Matches `pack!`, `pack_err!`, `pack_structural!`, `pack_err_structural!` invocations
+ - `GroupPattern` — Matches `group!` and `group_structural!` invocations
+ - `GrouppedDerivePattern` — Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]`
+ - `ChainPattern` — Matches `#[chain]` functions, extracts `__internal_chain_*` names
+ - `RendererPattern` — Matches `#[renderer]` functions, extracts `__internal_renderer_*` names
+ - `HelpPattern` — Matches `#[help]` functions, extracts `__internal_help_*` names
+ - `CompletionPattern` — Matches `#[completion(T)]` functions, extracts `__internal_completion_*` names
+ - `DispatcherPattern` — Matches `dispatcher!` invocations, extracts entry type names (supports both explicit and implicit forms)
+ - `DispatcherClapPattern` — Matches `#[dispatcher_clap]` structs, extracts struct names
+ - `type_mapping_builder` — Assembles the mapping from all analyzed files and writes `MAPPING` and `type_using.rs` output files
+
+- **Integration with `gen_program!()`** — When the `pathf` feature is enabled, `gen_program!()` includes the generated `type_using.rs` file via `include!()`, making all type paths available in scope for the generated dispatch code.
+
+- **Public re-exports** — The `mingling` crate re-exports `mingling_pathf` types under `mingling::pathf::*` and error types under `mingling::error::*` (behind the `pathf` feature gate).
+
#### **BREAKING CHANGES** (API CHANGES):
---
diff --git a/dev_tools/src/bin/test-examples.rs b/dev_tools/src/bin/test-examples.rs
index 5153709..539459e 100644
--- a/dev_tools/src/bin/test-examples.rs
+++ b/dev_tools/src/bin/test-examples.rs
@@ -94,7 +94,7 @@ fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize {
/// Build the example binary, return true on success
fn build_example(example_name: &str) -> bool {
let manifest = format!("examples/{example_name}/Cargo.toml");
- tools::run_cmd_capture(&format!(
+ tools::run_cmd_capture(format!(
"cargo build --manifest-path {manifest} --color always",
))
.is_ok()
diff --git a/dev_tools/src/lib.rs b/dev_tools/src/lib.rs
index 2bd55a2..bb4a7d9 100644
--- a/dev_tools/src/lib.rs
+++ b/dev_tools/src/lib.rs
@@ -255,7 +255,7 @@ pub fn run_parallel(phase: &str, tasks: Vec<(String, String, String)>) -> Result
code,
));
if !output.is_empty() {
- pb.println(output.trim_end().to_string());
+ pb.println(output.trim_end());
}
}
}
diff --git a/mingling_core/src/builds.rs b/mingling_core/src/builds.rs
index 51bafe6..17dc7d2 100644
--- a/mingling_core/src/builds.rs
+++ b/mingling_core/src/builds.rs
@@ -3,4 +3,7 @@
pub mod comp;
#[cfg(all(feature = "builds", feature = "pathf"))]
-pub use mingling_pathf::*;
+pub use mingling_pathf::analyze_and_build_type_mapping;
+
+#[cfg(all(feature = "builds", feature = "pathf"))]
+pub use mingling_pathf::analyze_and_build_type_mapping_for;
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index ef7d192..d49fa7f 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -44,6 +44,8 @@ pub mod error {
pub use crate::program::error::*;
#[cfg(feature = "structural_renderer")]
pub use crate::renderer::structural::error::*;
+ #[cfg(feature = "pathf")]
+ pub use mingling_pathf::error::*;
}
pub use crate::program::*;
@@ -92,3 +94,10 @@ pub mod core_res {
#[cfg(feature = "repl")]
pub use crate::program::repl_exec::res::ResREPL;
}
+
+#[cfg(feature = "pathf")]
+pub mod pathf {
+ pub use mingling_pathf::module_pathf::*;
+ pub use mingling_pathf::pattern_analyzer::*;
+ pub use mingling_pathf::patterns::*;
+}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 1b0cbb5..8955b37 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -2037,7 +2037,17 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream {
quote! { u128 }
};
+ let pathf_include = if cfg!(feature = "pathf") {
+ quote! {
+ include!(concat!(env!("OUT_DIR"), "/", env!("CARGO_PKG_NAME"), "/type_using.rs"));
+ }
+ } else {
+ quote! {}
+ };
+
let expanded = quote! {
+ #pathf_include
+
#[derive(Debug, PartialEq, Eq, Clone)]
#[repr(#repr_type)]
#[allow(nonstandard_style)]
diff --git a/mingling_pathf/README.md b/mingling_pathf/README.md
index 9706083..6d8857a 100644
--- a/mingling_pathf/README.md
+++ b/mingling_pathf/README.md
@@ -41,7 +41,7 @@ fn main() {
1. **Build-time scanning**: `build.rs` traverses all `.rs` source files under `src/`, locating macro invocations such as `pack!`, `#[chain]`, `#[renderer]`, etc., via pattern matching.
2. **Module inference**: The module path is inferred from the file's directory path (e.g., `src/app/sub.rs` → `app::sub`).
3. **Reference tracking**: Following the chain of `mod use` re-exports (i.e., paths re-exported via `pub use` or `use`), the type name is resolved to the module path under which it is ultimately referenced.
-4. **Mapping output**: The mapping from type names to their final referenceable module paths is written to `$OUT_DIR/CRATE_NAME/type-mapping.rs`.
+4. **Mapping output**: The mapping from type names to their final referenceable module paths is written to `$OUT_DIR/CRATE_NAME/type-mapping`.
5. **Compile-time consumption**: `gen_program!()` reads this mapping file and uses the full paths for downcasting in the generated dispatch code.
## Constraints
diff --git a/mingling_pathf/src/lib.rs b/mingling_pathf/src/lib.rs
index d8d81db..40fd2ec 100644
--- a/mingling_pathf/src/lib.rs
+++ b/mingling_pathf/src/lib.rs
@@ -2,3 +2,7 @@ pub mod module_pathf;
pub mod pattern_analyzer;
pub mod error;
pub mod patterns;
+
+mod type_mapping_builder;
+pub use type_mapping_builder::analyze_and_build_type_mapping;
+pub use type_mapping_builder::analyze_and_build_type_mapping_for;
diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs
index cb98a5f..4a1f8a4 100644
--- a/mingling_pathf/src/pattern_analyzer.rs
+++ b/mingling_pathf/src/pattern_analyzer.rs
@@ -16,6 +16,15 @@ pub fn init() -> PatternAnalyzer {
__register![
BasicStructPattern,
+ PackPattern,
+ GroupPattern,
+ GrouppedDerivePattern,
+ ChainPattern,
+ RendererPattern,
+ HelpPattern,
+ CompletionPattern,
+ DispatcherPattern,
+ DispatcherClapPattern,
];
analyzer
@@ -123,6 +132,22 @@ impl PatternAnalyzer {
/// - `Ok(HashSet<String>)` —— On success, returns a formatted set of strings, each in the form `"::module_path::item_name"`.
/// - `Err(MinglingPathfinderError)` —— If the file cannot be read, returns the corresponding I/O error wrapper.
pub fn analyze_file(&self, path: impl AsRef<Path>) -> Result<HashSet<String>, MinglingPathfinderError> {
+ self.collect_items(path).map(|items| {
+ AnalyzeResult { items }.into_formatted()
+ })
+ }
+
+ /// Analyzes a single file and returns the raw `Vec<AnalyzeItem>`.
+ ///
+ /// Unlike `analyze_file`, this method does not format the results into strings,
+ /// preserving the original module-path and item-name information. Useful for
+ /// callers that need to combine the results with other data sources.
+ pub fn analyze_file_items(&self, path: impl AsRef<Path>) -> Result<Vec<AnalyzeItem>, MinglingPathfinderError> {
+ self.collect_items(path)
+ }
+
+ /// Internal: collects raw `AnalyzeItem`s from a file.
+ fn collect_items(&self, path: impl AsRef<Path>) -> Result<Vec<AnalyzeItem>, MinglingPathfinderError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)?;
@@ -134,7 +159,6 @@ impl PatternAnalyzer {
}
}
- let result = AnalyzeResult { items: all_items };
- Ok(result.into_formatted())
+ Ok(all_items)
}
}
diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs
index 33d3503..b3e0cd3 100644
--- a/mingling_pathf/src/patterns.rs
+++ b/mingling_pathf/src/patterns.rs
@@ -1,2 +1,21 @@
-mod basic_struct;
pub use basic_struct::*;
+pub use chain::*;
+pub use completion::*;
+pub use dispatcher::*;
+pub use dispatcher_clap::*;
+pub use groupped_derive::*;
+pub use group::*;
+pub use help::*;
+pub use pack::*;
+pub use renderer::*;
+
+mod basic_struct;
+mod chain;
+mod completion;
+mod dispatcher;
+mod dispatcher_clap;
+mod groupped_derive;
+mod group;
+mod help;
+mod pack;
+mod renderer;
diff --git a/mingling_pathf/src/patterns/chain.rs b/mingling_pathf/src/patterns/chain.rs
new file mode 100644
index 0000000..10d698e
--- /dev/null
+++ b/mingling_pathf/src/patterns/chain.rs
@@ -0,0 +1,72 @@
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Match `#[chain]` functions, extract the generated internal struct name.
+///
+/// `#[chain] fn handle_greet(...)` → `__internal_chain_handle_greet`
+///
+/// Covered forms:
+/// - `#[chain] fn handle(args: EntryType) -> Next { ... }`
+/// - `#[chain] fn handle(args: EntryType, res: &mut Res) -> Next { ... }`
+/// - async version
+pub struct ChainPattern;
+
+impl AnalyzePattern for ChainPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("chain]")
+ }
+
+ 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 {
+ collect_from_item(item, "", &mut items);
+ }
+
+ items
+ }
+}
+
+fn internal_name(fn_name: &str) -> String {
+ format!("__internal_chain_{fn_name}")
+}
+
+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),
+ });
+ }
+ Item::Mod(item_mod) => {
+ if let Some((_, nested)) = &item_mod.content {
+ let mod_name = &item_mod.ident.to_string();
+ let nested_mod = if current_mod.is_empty() {
+ mod_name.clone()
+ } else {
+ format!("{current_mod}::{mod_name}")
+ };
+ for n in nested {
+ collect_from_item(n, &nested_mod, items);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
+ attrs.iter().any(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|s| s.ident == name)
+ })
+}
diff --git a/mingling_pathf/src/patterns/completion.rs b/mingling_pathf/src/patterns/completion.rs
new file mode 100644
index 0000000..7e4cd09
--- /dev/null
+++ b/mingling_pathf/src/patterns/completion.rs
@@ -0,0 +1,65 @@
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Matches `#[completion(T)]` functions, extracts the generated inner struct name.
+///
+/// `#[completion(EntryGreet)] fn complete_greet_entry(...)` → `__internal_completion_complete_greet_entry`
+pub struct CompletionPattern;
+
+impl AnalyzePattern for CompletionPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("completion(")
+ }
+
+ 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 {
+ collect_from_item(item, "", &mut items);
+ }
+ items
+ }
+}
+
+fn internal_name(fn_name: &str) -> String {
+ format!("__internal_completion_{fn_name}")
+}
+
+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),
+ });
+ }
+ Item::Mod(item_mod) => {
+ if let Some((_, nested)) = &item_mod.content {
+ let mod_name = &item_mod.ident.to_string();
+ let nested_mod = if current_mod.is_empty() {
+ mod_name.clone()
+ } else {
+ format!("{current_mod}::{mod_name}")
+ };
+ for n in nested {
+ collect_from_item(n, &nested_mod, items);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
+ attrs.iter().any(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|s| s.ident == name)
+ })
+}
diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs
new file mode 100644
index 0000000..7bb076c
--- /dev/null
+++ b/mingling_pathf/src/patterns/dispatcher.rs
@@ -0,0 +1,116 @@
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Matches the `dispatcher!` macro, extracts the entry type name.
+///
+/// Supported forms:
+/// - `dispatcher!("greet", CMDGreet => EntryGreet)` — explicit
+/// - `dispatcher!("greet")` — implicit, infers EntryType
+/// - `dispatcher! { ... }` — with braces
+pub struct DispatcherPattern;
+
+impl AnalyzePattern for DispatcherPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("dispatcher!")
+ }
+
+ 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::Macro(m) => {
+ let macro_name = macro_simple_name(m);
+ if macro_name != "dispatcher" {
+ continue;
+ }
+ if let Some(entry_name) = extract_dispatcher_entry(&m.mac.tokens) {
+ items.push(AnalyzeItem {
+ module: String::new(),
+ item_name: entry_name,
+ });
+ }
+ }
+ 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;
+ }
+ if let Some(entry_name) = extract_dispatcher_entry(&m.mac.tokens) {
+ items.push(AnalyzeItem {
+ module: item_mod.ident.to_string(),
+ item_name: entry_name,
+ });
+ }
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ items
+ }
+}
+
+fn macro_simple_name(m: &syn::ItemMacro) -> String {
+ m.mac
+ .path
+ .segments
+ .last()
+ .map(|s| s.ident.to_string())
+ .unwrap_or_default()
+}
+
+/// Extracts the entry type name from the `dispatcher!` macro arguments.
+///
+/// Input examples:
+/// - `"greet", CMDGreet => EntryGreet` → `EntryGreet`
+/// - `"remote.add"` → `EntryRemoteAdd` (implicit inference)
+fn extract_dispatcher_entry(tokens: &proc_macro2::TokenStream) -> Option<String> {
+ let stream = tokens.to_string();
+
+ // Explicit form: look for `=>`
+ if let Some(arrow_idx) = stream.find("=>") {
+ let after_arrow = stream[arrow_idx + 2..].trim();
+ let entry_name = after_arrow
+ .split(|c: char| c.is_whitespace() || c == ',' || c == ')' || c == '}')
+ .next()?;
+ if !entry_name.is_empty() {
+ return Some(entry_name.trim().to_string());
+ }
+ }
+
+ // Implicit form: infer from command name
+ let stream = stream.trim();
+ if let Some(start) = stream.find('"') {
+ let rest = &stream[start + 1..];
+ let cmd_name = rest.split('"').next()?;
+ let last_segment = cmd_name.split('.').next_back()?;
+ let entry = format!("Entry{}", to_pascal_case(last_segment));
+ Some(entry)
+ } else {
+ None
+ }
+}
+
+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::<String>() + c.as_str(),
+ }
+ })
+ .collect()
+}
diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs
new file mode 100644
index 0000000..398b269
--- /dev/null
+++ b/mingling_pathf/src/patterns/dispatcher_clap.rs
@@ -0,0 +1,60 @@
+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).
+///
+/// Covers the following forms:
+/// - `#[dispatcher_clap] struct EntryGreet { ... }`
+/// - `#[dispatcher_clap] #[command(...)] struct EntryGreet { ... }`
+pub struct DispatcherClapPattern;
+
+impl AnalyzePattern for DispatcherClapPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("dispatcher_clap(")
+ }
+
+ 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_attr(&s.attrs, "dispatcher_clap") => {
+ items.push(AnalyzeItem {
+ module: String::new(),
+ item_name: s.ident.to_string(),
+ });
+ }
+ 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(),
+ });
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ items
+ }
+}
+
+fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
+ attrs.iter().any(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|s| s.ident == name)
+ })
+}
diff --git a/mingling_pathf/src/patterns/group.rs b/mingling_pathf/src/patterns/group.rs
new file mode 100644
index 0000000..99d1137
--- /dev/null
+++ b/mingling_pathf/src/patterns/group.rs
@@ -0,0 +1,101 @@
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Matches the `group!` and `group_structural!` macros.
+///
+/// Covered forms:
+/// - `group!(TypeName)`
+/// - `group!(Alias = path::Type)`
+/// - `group_structural!(TypeName)`
+/// - `group_structural!(Alias = path::Type)`
+pub struct GroupPattern;
+
+impl AnalyzePattern for GroupPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("group!") || content.contains("group_structural!")
+ }
+
+ 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::Macro(m) => {
+ let Some(last) = m.mac.path.segments.last() else {
+ continue;
+ };
+ let macro_name = last.ident.to_string();
+ 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,
+ });
+ }
+ }
+ Item::Mod(item_mod) => {
+ if let Some((_, nested)) = &item_mod.content {
+ for n in nested {
+ if let Item::Macro(m) = n {
+ let Some(last) = m.mac.path.segments.last() else {
+ continue;
+ };
+ let macro_name = last.ident.to_string();
+ 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,
+ });
+ }
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ items
+ }
+}
+
+/// Extract the alias / type name from the arguments of `group!`.
+///
+/// - `group!(ParseIntError)` → `ParseIntError`
+/// - `group!(ErrorIo = std::io::Error)` → `ErrorIo`
+fn extract_group_name(tokens: &proc_macro2::TokenStream) -> Option<String> {
+ let stream = tokens.clone();
+ let mut iter = stream.into_iter();
+
+ loop {
+ match iter.next()? {
+ proc_macro2::TokenTree::Ident(ident) => {
+ let name = ident.to_string();
+
+ // Check if there is a `=` following
+ let next = iter.next();
+ match next {
+ Some(proc_macro2::TokenTree::Punct(p)) if p.as_char() == '=' => {
+ // group!(Alias = path::Type)
+ return Some(name);
+ }
+ _ => {
+ // group!(TypeName)
+ return Some(name);
+ }
+ }
+ }
+ _ => continue,
+ }
+ }
+}
diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/groupped_derive.rs
new file mode 100644
index 0000000..9f7301d
--- /dev/null
+++ b/mingling_pathf/src/patterns/groupped_derive.rs
@@ -0,0 +1,93 @@
+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") {
+ attr.parse_args::<syn::MetaList>().ok().is_some_and(|meta| {
+ meta.path.segments.iter().any(|seg| {
+ let name = seg.ident.to_string();
+ name == "Groupped" || name == "GrouppedSerialize"
+ })
+ })
+ } else {
+ false
+ }
+ })
+}
diff --git a/mingling_pathf/src/patterns/help.rs b/mingling_pathf/src/patterns/help.rs
new file mode 100644
index 0000000..357626b
--- /dev/null
+++ b/mingling_pathf/src/patterns/help.rs
@@ -0,0 +1,65 @@
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Matches `#[help]` functions, extracting the generated internal struct name.
+///
+/// `#[help] fn help_my_entry(...)` → `__internal_help_help_my_entry`
+pub struct HelpPattern;
+
+impl AnalyzePattern for HelpPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("help]")
+ }
+
+ 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 {
+ collect_from_item(item, "", &mut items);
+ }
+ items
+ }
+}
+
+fn internal_name(fn_name: &str) -> String {
+ format!("__internal_help_{fn_name}")
+}
+
+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),
+ });
+ }
+ Item::Mod(item_mod) => {
+ if let Some((_, nested)) = &item_mod.content {
+ let mod_name = &item_mod.ident.to_string();
+ let nested_mod = if current_mod.is_empty() {
+ mod_name.clone()
+ } else {
+ format!("{current_mod}::{mod_name}")
+ };
+ for n in nested {
+ collect_from_item(n, &nested_mod, items);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
+ attrs.iter().any(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|s| s.ident == name)
+ })
+}
diff --git a/mingling_pathf/src/patterns/pack.rs b/mingling_pathf/src/patterns/pack.rs
new file mode 100644
index 0000000..f025f7d
--- /dev/null
+++ b/mingling_pathf/src/patterns/pack.rs
@@ -0,0 +1,103 @@
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Matches types defined by `pack!`, `pack_err!`, `pack_structural!`, `pack_err_structural!` macros.
+///
+/// Covered forms:
+/// - `pack!(TypeName = InnerType)`
+/// - `pack! { TypeName = InnerType }`
+/// - `pack_err!(TypeName)`
+/// - `pack_err!(TypeName = InnerType)`
+/// - `pack_structural!` series same as above
+pub struct PackPattern;
+
+impl AnalyzePattern for PackPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("pack!") || content.contains("pack_err!")
+ }
+
+ 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 {
+ // 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,
+ });
+ }
+ }
+ // Macro calls inside inline modules
+ Item::Mod(item_mod) => {
+ if let Some((_, nested)) = &item_mod.content {
+ for n in nested {
+ 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
+ }
+}
+
+/// If the macro call is `pack!` / `pack_err!` / etc., extract the registered type name.
+fn try_extract_pack_name(m: &syn::ItemMacro) -> Option<String> {
+ let macro_name = m.mac.path.segments.last()?.ident.to_string();
+
+ match macro_name.as_str() {
+ "pack" | "pack_err" | "pack_structural" | "pack_err_structural" => {}
+ _ => return None,
+ }
+
+ let tokens = &m.mac.tokens;
+
+ // `pack!(T)` or `pack!(T = U)` — the first ident is the type name
+ // Parse simply with syn
+ if let Ok(ident) = syn::parse2::<syn::Ident>(tokens.clone()) {
+ // pack!(TypeName) — just a single ident
+ return Some(ident.to_string());
+ }
+
+ // Try to parse `Ident = Type`
+ // Clone tokens first to avoid partial consumption
+ let stream = tokens.clone();
+ let mut iter = stream.into_iter();
+
+ // Skip leading attributes/doc comments
+ loop {
+ match iter.next()? {
+ proc_macro2::TokenTree::Ident(ident) => {
+ // Found the first ident, this is the type name
+ let type_name = ident.to_string();
+
+ // Check if `=` follows
+ if let Some(proc_macro2::TokenTree::Punct(p)) = iter.next()
+ && p.as_char() == '=' {
+ // pack!(TypeName = InnerType)
+ return Some(type_name);
+ }
+
+ // pack_err!(TypeName) — only a single ident
+ return Some(type_name);
+ }
+ _ => continue,
+ }
+ }
+}
diff --git a/mingling_pathf/src/patterns/renderer.rs b/mingling_pathf/src/patterns/renderer.rs
new file mode 100644
index 0000000..410ae14
--- /dev/null
+++ b/mingling_pathf/src/patterns/renderer.rs
@@ -0,0 +1,65 @@
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Match `#[renderer]` functions, extract the generated internal struct name.
+///
+/// `#[renderer] fn render_name(...)` → `__internal_renderer_render_name`
+pub struct RendererPattern;
+
+impl AnalyzePattern for RendererPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("renderer]")
+ }
+
+ 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 {
+ collect_from_item(item, "", &mut items);
+ }
+ items
+ }
+}
+
+fn internal_name(fn_name: &str) -> String {
+ format!("__internal_renderer_{fn_name}")
+}
+
+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),
+ });
+ }
+ Item::Mod(item_mod) => {
+ if let Some((_, nested)) = &item_mod.content {
+ let mod_name = &item_mod.ident.to_string();
+ let nested_mod = if current_mod.is_empty() {
+ mod_name.clone()
+ } else {
+ format!("{current_mod}::{mod_name}")
+ };
+ for n in nested {
+ collect_from_item(n, &nested_mod, items);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
+ attrs.iter().any(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|s| s.ident == name)
+ })
+}
diff --git a/mingling_pathf/src/type_mapping_builder.rs b/mingling_pathf/src/type_mapping_builder.rs
new file mode 100644
index 0000000..c701536
--- /dev/null
+++ b/mingling_pathf/src/type_mapping_builder.rs
@@ -0,0 +1,101 @@
+use std::collections::HashSet;
+use std::path::{Path};
+
+use crate::error::MinglingPathfinderError;
+use crate::module_pathf;
+use crate::pattern_analyzer;
+
+/// Analyzes the Mingling types of the specified crate directory and generates mapping files to the specified output directory.
+///
+/// `crate_dir` — crate root directory (i.e., the directory containing Cargo.toml)
+/// `output_dir` — directory where mapping files will be written
+///
+/// Mapping file format per line: `TypeName = crate::module::path::TypeName`
+pub fn analyze_and_build_type_mapping_for(
+ crate_dir: &Path,
+ output_dir: &Path,
+) -> Result<(), MinglingPathfinderError> {
+ let module_mapping = module_pathf::analyze(crate_dir)?;
+ let analyzer = pattern_analyzer::init();
+
+ let mut type_mappings: Vec<(String, String)> = Vec::new();
+
+ for item in module_mapping {
+ let file_abs = crate_dir.join(item.file_path());
+ if !file_abs.is_file() {
+ continue;
+ }
+
+ let module_path = item.module_path();
+ let Ok(analyze_items) = analyzer.analyze_file_items(&file_abs) else {
+ continue;
+ };
+
+ for ai in analyze_items {
+ let full_path = if ai.module.is_empty() {
+ format!("{}::{}", module_path, ai.item_name)
+ } else {
+ format!("{}::{}::{}", module_path, ai.module, ai.item_name)
+ };
+ type_mappings.push((ai.item_name, full_path));
+ }
+ }
+
+ // Sort by full path (ASCII order)
+ type_mappings.sort_by(|a, b| a.1.cmp(&b.1));
+
+ // Deduplicate by type name, keeping the first occurrence
+ let mut seen = HashSet::new();
+ type_mappings.retain(|(name, _)| seen.insert(name.clone()));
+
+ // Create output directory
+ std::fs::create_dir_all(output_dir)?;
+
+ // Write files
+ let output_path = output_dir.join("MAPPING");
+ let type_using_path = output_dir.join("type_using.rs");
+
+ let mut content_mapping = String::new();
+ for (name, path) in &type_mappings {
+ content_mapping.push_str(&format!("{name} = {path}\n"));
+ }
+ std::fs::write(&output_path, content_mapping)?;
+
+ let mut content_using = String::new();
+ for (_, path) in &type_mappings {
+ content_using.push_str(&format!("use {path};\n"));
+ }
+ std::fs::write(&type_using_path, content_using)?;
+
+ Ok(())
+}
+
+/// Convenience version to be called from `build.rs`, automatically reading configuration
+/// from environment variables.
+///
+/// Reads `CARGO_PKG_NAME` and `OUT_DIR`, and outputs to `{OUT_DIR}/{CARGO_PKG_NAME}/`.
+pub fn analyze_and_build_type_mapping() -> Result<(), MinglingPathfinderError> {
+ let crate_name = std::env::var("CARGO_PKG_NAME")
+ .map_err(|_| MinglingPathfinderError::IoError(
+ std::io::Error::new(std::io::ErrorKind::NotFound,
+ "CARGO_PKG_NAME not set (not running in build.rs?)")
+ ))?;
+
+ let out_dir = std::env::var("OUT_DIR")
+ .map_err(|_| MinglingPathfinderError::IoError(
+ std::io::Error::new(std::io::ErrorKind::NotFound,
+ "OUT_DIR not set (not running in build.rs?)")
+ ))?;
+
+ let crate_dir = std::env::current_dir()?;
+ let output_dir = Path::new(&out_dir).join(&crate_name);
+
+ analyze_and_build_type_mapping_for(&crate_dir, &output_dir)?;
+
+ // Notify Cargo to re-run build.rs when source files change
+ println!("cargo:rerun-if-changed=src/");
+ println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS");
+ println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_ARCH");
+
+ Ok(())
+}
diff --git a/mingling_pathf/test/src/.gitignore b/mingling_pathf/test/src/.gitignore
new file mode 100644
index 0000000..13bd6e1
--- /dev/null
+++ b/mingling_pathf/test/src/.gitignore
@@ -0,0 +1 @@
+main.rs
diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs
index f2ca3e0..2fcf01a 100644
--- a/mingling_pathf/test/src/lib.rs
+++ b/mingling_pathf/test/src/lib.rs
@@ -1,6 +1,7 @@
#![cfg(test)]
use std::{collections::HashMap, env::current_dir};
+use mingling_pathf::analyze_and_build_type_mapping_for;
#[test]
fn test_module_pathf() {
@@ -46,3 +47,237 @@ fn test_pattern_analyzer_once() {
let result = analyzer.analyze_file(dir.join("src/has_sub_mod.rs")).unwrap();
assert!(result.contains("::directly_sub_mod::DirectlySubModStruct"));
}
+
+#[test]
+fn test_chain_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_chain.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required_entries: Vec<&str> = vec![
+ "::sub::__internal_chain_my_chain1",
+ "::sub::__internal_chain_my_chain2",
+ "::sub::__internal_chain_my_chain3",
+ "::sub::__internal_chain_my_chain4",
+ "::sub::__internal_chain_my_chain5",
+ "::sub::__internal_chain_my_chain6",
+ "::__internal_chain_my_chain1",
+ "::__internal_chain_my_chain2",
+ "::__internal_chain_my_chain3",
+ "::__internal_chain_my_chain4",
+ "::__internal_chain_my_chain5",
+ "::__internal_chain_my_chain6",
+ ];
+
+ assert_eq!(r.len(), required_entries.len(), "Result should contain exactly {} entries", required_entries.len());
+
+ for entry in &required_entries {
+ assert!(r.iter().any(|e| e == entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_renderer_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_renderer.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::sub::__internal_renderer_my_renderer1",
+ "::sub::__internal_renderer_my_renderer2",
+ "::sub::__internal_renderer_my_renderer3",
+ "::sub::__internal_renderer_my_renderer4",
+ "::__internal_renderer_my_renderer1",
+ "::__internal_renderer_my_renderer2",
+ "::__internal_renderer_my_renderer3",
+ "::__internal_renderer_my_renderer4",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_help_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_help.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::sub::__internal_help_my_help1",
+ "::sub::__internal_help_my_help2",
+ "::sub::__internal_help_my_help3",
+ "::sub::__internal_help_my_help4",
+ "::__internal_help_my_help1",
+ "::__internal_help_my_help2",
+ "::__internal_help_my_help3",
+ "::__internal_help_my_help4",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_completion_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_completion.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::sub::__internal_completion_my_completion1",
+ "::sub::__internal_completion_my_completion2",
+ "::sub::__internal_completion_my_completion3",
+ "::sub::__internal_completion_my_completion4",
+ "::__internal_completion_my_completion1",
+ "::__internal_completion_my_completion2",
+ "::__internal_completion_my_completion3",
+ "::__internal_completion_my_completion4",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_pack_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_pack.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::ResultPack1",
+ "::ErrorPack1",
+ "::ErrorPack2",
+ "::ResultPack2",
+ "::ErrorPack3",
+ "::ErrorPack4",
+ "::sub::ResultPack1",
+ "::sub::ErrorPack1",
+ "::sub::ErrorPack2",
+ "::sub::ResultPack2",
+ "::sub::ErrorPack3",
+ "::sub::ErrorPack4",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_group_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_group.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::Group1",
+ "::GroupAlias1",
+ "::Group2",
+ "::GroupAlias2",
+ "::sub::Group1",
+ "::sub::GroupAlias1",
+ "::sub::Group2",
+ "::sub::GroupAlias2",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_groupped_derive_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_groupped_derive.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::Derived1",
+ "::Derived2",
+ "::Derived3",
+ "::sub::Derived1",
+ "::sub::Derived3",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_dispatcher_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_dispatcher.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::EntryGreet",
+ "::EntryRemoteAdd",
+ "::EntryAdd",
+ "::EntryDelete",
+ "::EntryRemoteRm",
+ "::EntryRm",
+ "::sub::EntryGreet",
+ "::sub::EntryDelete",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_dispatcher_clap_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir().unwrap().join("src/test_files/test_dispatcher_clap.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ "::EntryClap1",
+ "::EntryClap2",
+ "::EntryClap3",
+ "::EntryClap4",
+ "::sub::EntryClap1",
+ "::sub::EntryClap3",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {}", entry);
+ }
+}
+
+#[test]
+fn test_type_mapping_file_created() {
+ let tmp = std::env::temp_dir().join("mingling_pathf_test_type_mapping");
+ let _ = std::fs::remove_dir_all(&tmp);
+
+ let crate_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
+
+ analyze_and_build_type_mapping_for(
+ crate_dir,
+ &tmp,
+ )
+ .unwrap();
+
+ let output_path = tmp.join("type-mapping");
+ assert!(
+ output_path.exists(),
+ "type-mapping file should exist at: {}",
+ output_path.display()
+ );
+
+ let _ = std::fs::remove_dir_all(&tmp);
+}
diff --git a/mingling_pathf/test/src/test_files/test_chain.rs b/mingling_pathf/test/src/test_files/test_chain.rs
new file mode 100644
index 0000000..e209a5e
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_chain.rs
@@ -0,0 +1,61 @@
+#[mingling::macros::chain]
+fn my_chain1(prev: Some1) -> Next {
+
+}
+
+#[mingling::macros::chain]
+pub fn my_chain2(prev: Some2) -> Next {
+
+}
+
+#[mingling::macros::chain]
+pub async fn my_chain3(prev: Some3) -> Next {
+
+}
+
+#[chain]
+fn my_chain4(prev: Some4) {
+
+}
+
+#[chain]
+pub fn my_chain5(prev: Some5) {
+
+}
+
+#[chain]
+pub async fn my_chain6(prev: Some6) {
+
+}
+
+pub mod sub {
+ #[mingling::macros::chain]
+ fn my_chain1(prev: Some1) -> Next {
+
+ }
+
+ #[mingling::macros::chain]
+ pub fn my_chain2(prev: Some2) -> Next {
+
+ }
+
+ #[mingling::macros::chain]
+ pub async fn my_chain3(prev: Some3) -> Next {
+
+ }
+
+ #[chain]
+ fn my_chain4(prev: Some4) {
+
+ }
+
+ #[chain]
+ pub fn my_chain5(prev: Some5) {
+
+ }
+
+ #[chain]
+ pub async fn my_chain6(prev: Some6) {
+
+ }
+}
diff --git a/mingling_pathf/test/src/test_files/test_completion.rs b/mingling_pathf/test/src/test_files/test_completion.rs
new file mode 100644
index 0000000..87a655f
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_completion.rs
@@ -0,0 +1,41 @@
+#[mingling::macros::completion(Some1)]
+fn my_completion1(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+}
+
+#[mingling::macros::completion(Some2)]
+pub fn my_completion2(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+}
+
+#[completion(Some3)]
+fn my_completion3(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+}
+
+#[completion(Some4)]
+pub fn my_completion4(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+}
+
+pub mod sub {
+ #[mingling::macros::completion(Some1)]
+ fn my_completion1(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+ }
+
+ #[mingling::macros::completion(Some2)]
+ pub fn my_completion2(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+ }
+
+ #[completion(Some3)]
+ fn my_completion3(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+ }
+
+ #[completion(Some4)]
+ pub fn my_completion4(ctx: &mingling::ShellContext) -> mingling::Suggest {
+ mingling::Suggest::new()
+ }
+}
diff --git a/mingling_pathf/test/src/test_files/test_dispatcher.rs b/mingling_pathf/test/src/test_files/test_dispatcher.rs
new file mode 100644
index 0000000..48f5e4d
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_dispatcher.rs
@@ -0,0 +1,17 @@
+mingling::macros::dispatcher!("greet", CMDGreet => EntryGreet);
+mingling::macros::dispatcher!("greet");
+mingling::macros::dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+mingling::macros::dispatcher!("remote.add");
+
+dispatcher!("delete", CMDDelete => EntryDelete);
+dispatcher!("delete");
+dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm);
+dispatcher!("remote.rm");
+
+pub mod sub {
+ mingling::macros::dispatcher!("greet", CMDGreet => EntryGreet);
+ mingling::macros::dispatcher!("greet");
+
+ dispatcher!("delete", CMDDelete => EntryDelete);
+ dispatcher!("delete");
+}
diff --git a/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs
new file mode 100644
index 0000000..0ba884d
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs
@@ -0,0 +1,33 @@
+#[mingling::macros::dispatcher_clap]
+struct EntryClap1 {
+ name: String,
+ age: i32,
+}
+
+#[mingling::macros::dispatcher_clap]
+#[command(name = "greet")]
+pub struct EntryClap2 {
+ name: String,
+}
+
+#[dispatcher_clap]
+struct EntryClap3 {
+ value: String,
+}
+
+#[dispatcher_clap]
+pub struct EntryClap4 {
+ value: i32,
+}
+
+pub mod sub {
+ #[mingling::macros::dispatcher_clap]
+ struct EntryClap1 {
+ name: String,
+ }
+
+ #[dispatcher_clap]
+ struct EntryClap3 {
+ value: String,
+ }
+}
diff --git a/mingling_pathf/test/src/test_files/test_group.rs b/mingling_pathf/test/src/test_files/test_group.rs
new file mode 100644
index 0000000..92c8cda
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_group.rs
@@ -0,0 +1,13 @@
+mingling::macros::group!(Group1);
+mingling::macros::group!(GroupAlias1 = std::io::Error);
+
+group!(Group2);
+group!(GroupAlias2 = std::num::ParseIntError);
+
+pub mod sub {
+ mingling::macros::group!(Group1);
+ mingling::macros::group!(GroupAlias1 = std::io::Error);
+
+ group!(Group2);
+ group!(GroupAlias2 = std::num::ParseIntError);
+}
diff --git a/mingling_pathf/test/src/test_files/test_groupped_derive.rs b/mingling_pathf/test/src/test_files/test_groupped_derive.rs
new file mode 100644
index 0000000..f6c6fa9
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_groupped_derive.rs
@@ -0,0 +1,26 @@
+#[derive(Groupped)]
+struct Derived1 {
+ value: String,
+}
+
+#[derive(Groupped, Debug, Clone)]
+struct Derived2 {
+ value: i32,
+}
+
+#[derive(GrouppedSerialize)]
+struct Derived3 {
+ value: bool,
+}
+
+pub mod sub {
+ #[derive(Groupped)]
+ struct Derived1 {
+ value: String,
+ }
+
+ #[derive(GrouppedSerialize)]
+ struct Derived3 {
+ value: bool,
+ }
+}
diff --git a/mingling_pathf/test/src/test_files/test_help.rs b/mingling_pathf/test/src/test_files/test_help.rs
new file mode 100644
index 0000000..52d1408
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_help.rs
@@ -0,0 +1,33 @@
+#[mingling::macros::help]
+fn my_help1(prev: Some1) {
+}
+
+#[mingling::macros::help]
+pub fn my_help2(prev: Some2) {
+}
+
+#[help]
+fn my_help3(prev: Some3) {
+}
+
+#[help]
+pub fn my_help4(prev: Some4) {
+}
+
+pub mod sub {
+ #[mingling::macros::help]
+ fn my_help1(prev: Some1) {
+ }
+
+ #[mingling::macros::help]
+ pub fn my_help2(prev: Some2) {
+ }
+
+ #[help]
+ fn my_help3(prev: Some3) {
+ }
+
+ #[help]
+ pub fn my_help4(prev: Some4) {
+ }
+}
diff --git a/mingling_pathf/test/src/test_files/test_pack.rs b/mingling_pathf/test/src/test_files/test_pack.rs
new file mode 100644
index 0000000..759e35f
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_pack.rs
@@ -0,0 +1,17 @@
+mingling::macros::pack!(ResultPack1 = String);
+mingling::macros::pack_err!(ErrorPack1);
+mingling::macros::pack_err!(ErrorPack2 = PathBuf);
+
+pack!(ResultPack2 = (u8, String));
+pack_err!(ErrorPack3);
+pack_err!(ErrorPack4 = PathBuf);
+
+pub mod sub {
+ mingling::macros::pack!(ResultPack1 = String);
+ mingling::macros::pack_err!(ErrorPack1);
+ mingling::macros::pack_err!(ErrorPack2 = PathBuf);
+
+ pack!(ResultPack2 = (u8, String));
+ pack_err!(ErrorPack3);
+ pack_err!(ErrorPack4 = PathBuf);
+}
diff --git a/mingling_pathf/test/src/test_files/test_renderer.rs b/mingling_pathf/test/src/test_files/test_renderer.rs
new file mode 100644
index 0000000..ea52f5c
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_renderer.rs
@@ -0,0 +1,33 @@
+#[mingling::macros::renderer]
+fn my_renderer1(prev: Some1) {
+}
+
+#[mingling::macros::renderer]
+pub fn my_renderer2(prev: Some2) {
+}
+
+#[renderer]
+fn my_renderer3(prev: Some3) {
+}
+
+#[renderer]
+pub fn my_renderer4(prev: Some4) {
+}
+
+pub mod sub {
+ #[mingling::macros::renderer]
+ fn my_renderer1(prev: Some1) {
+ }
+
+ #[mingling::macros::renderer]
+ pub fn my_renderer2(prev: Some2) {
+ }
+
+ #[renderer]
+ fn my_renderer3(prev: Some3) {
+ }
+
+ #[renderer]
+ pub fn my_renderer4(prev: Some4) {
+ }
+}