aboutsummaryrefslogtreecommitdiff
path: root/mingling_pathf/src/patterns
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/patterns
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/patterns')
-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
9 files changed, 740 insertions, 0 deletions
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)
+ })
+}