diff options
Diffstat (limited to 'mingling_pathf/src')
| -rw-r--r-- | mingling_pathf/src/lib.rs | 4 | ||||
| -rw-r--r-- | mingling_pathf/src/pattern_analyzer.rs | 28 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns.rs | 21 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/chain.rs | 72 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/completion.rs | 65 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/dispatcher.rs | 116 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/dispatcher_clap.rs | 60 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/group.rs | 101 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/groupped_derive.rs | 93 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/help.rs | 65 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/pack.rs | 103 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/renderer.rs | 65 | ||||
| -rw-r--r-- | mingling_pathf/src/type_mapping_builder.rs | 101 |
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(()) +} |
