aboutsummaryrefslogtreecommitdiff
path: root/mingling_pathf/src
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 /mingling_pathf/src
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!()`.
Diffstat (limited to 'mingling_pathf/src')
-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
13 files changed, 891 insertions, 3 deletions
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(())
+}