aboutsummaryrefslogtreecommitdiff
path: root/mingling_pathf
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-04 13:26:48 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-04 13:26:48 +0800
commit4588e17f7ddacd8391a5c881a209735e08d48797 (patch)
tree2b513f4d4f8a18ebd3a34cff7c0fb8abf5787321 /mingling_pathf
parentf7862affd74ac3b129f9e9791a4cd4e1c7e3e6d1 (diff)
feat: add entry metadata system with compile-time typed values
Add `Metadata<B>` trait and `#[metadata(Entry)]` attribute macro to attach arbitrary, compile-time-typed metadata to entries. Implement `ProgramCollect::get_metadata<T>()` for runtime retrieval, backed by a global registry populated by `register_metadata!`. Extend `pathf` with `MetadataPattern` to resolve metadata types across modules at build time. Add two examples demonstrating metadata usage with and without pathf integration.
Diffstat (limited to 'mingling_pathf')
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs1
-rw-r--r--mingling_pathf/src/patterns.rs2
-rw-r--r--mingling_pathf/src/patterns/metadata.rs165
-rw-r--r--mingling_pathf/test/src/lib.rs33
-rw-r--r--mingling_pathf/test/src/test_files/test_metadata.rs40
5 files changed, 241 insertions, 0 deletions
diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs
index 79b1c5a..4675dd1 100644
--- a/mingling_pathf/src/pattern_analyzer.rs
+++ b/mingling_pathf/src/pattern_analyzer.rs
@@ -36,6 +36,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer {
analyzer.add_pattern(CommandPattern);
analyzer.add_pattern(RendererPattern);
analyzer.add_pattern(HelpPattern);
+ analyzer.add_pattern(MetadataPattern);
analyzer.add_pattern(CompletionPattern);
analyzer.add_pattern(DispatcherPattern::new(config.use_dispatch_tree));
analyzer.add_pattern(DispatcherClapPattern::new(config.use_dispatch_tree));
diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs
index 964fe7c..fcc50c3 100644
--- a/mingling_pathf/src/patterns.rs
+++ b/mingling_pathf/src/patterns.rs
@@ -9,6 +9,7 @@ pub use dispatcher_clap::*;
pub use group::*;
pub use grouped_derive::*;
pub use help::*;
+pub use metadata::*;
pub use pack::*;
pub use renderer::*;
@@ -21,5 +22,6 @@ mod dispatcher_clap;
mod group;
mod grouped_derive;
mod help;
+mod metadata;
mod pack;
mod renderer;
diff --git a/mingling_pathf/src/patterns/metadata.rs b/mingling_pathf/src/patterns/metadata.rs
new file mode 100644
index 0000000..24243bf
--- /dev/null
+++ b/mingling_pathf/src/patterns/metadata.rs
@@ -0,0 +1,165 @@
+//! The `MetadataPattern` matches functions annotated with `#[metadata(BindType)]`
+//! and extracts two types referenced by the metadata system:
+//! - `BindType` — the entry enum variant the metadata is bound to (attribute argument)
+//! - `DataType` — the function's return type, i.e. the metadata type
+//!
+//! Both types are tracked so that `pathf` can emit the `use` statements needed to
+//! bring them into scope for `gen_program!` generated code.
+//!
+//! Example:
+//! ```ignore
+//! #[metadata(EntryGreet)] // BindType = EntryGreet
+//! pub fn get_desc() -> Description { ... } // DataType = Description
+//! ```
+
+use std::collections::HashMap;
+
+use syn::Item;
+use syn::UseTree;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Matches `#[metadata(BindType)]` functions, extracting the bound entry type
+/// and the metadata (return) type.
+pub struct MetadataPattern;
+
+impl AnalyzePattern for MetadataPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("[metadata(") || content.contains("[metadata]")
+ }
+
+ fn analyze(&self, content: &str) -> Vec<AnalyzeItem> {
+ let Ok(syntax) = syn::parse_file(content) else {
+ return Vec::new();
+ };
+
+ let imports = collect_use_imports(&syntax.items);
+
+ let mut items = Vec::new();
+ for item in &syntax.items {
+ collect_from_item(item, "", &imports, &mut items);
+ }
+ items
+ }
+}
+
+fn collect_from_item(
+ item: &Item,
+ current_mod: &str,
+ imports: &HashMap<String, (String, String)>,
+ items: &mut Vec<AnalyzeItem>,
+) {
+ match item {
+ Item::Fn(f) => {
+ let Some(bind_type) = extract_bind_type(&f.attrs) else {
+ return;
+ };
+ let data_type = extract_data_type(f);
+ let Some(data_type) = data_type else {
+ return;
+ };
+
+ // BindType — always an in-crate entry type generated by dispatcher!/pack!.
+ items.push(AnalyzeItem::local(current_mod.to_string(), bind_type));
+
+ // DataType — may be a local type or a `use`-imported foreign type.
+ if let Some((module, _)) = imports.get(&data_type) {
+ items.push(AnalyzeItem::foreign(module.clone(), data_type));
+ } else {
+ items.push(AnalyzeItem::local(current_mod.to_string(), data_type));
+ }
+ }
+ 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}")
+ };
+ let inner_imports = collect_use_imports(nested);
+ for n in nested {
+ collect_from_item(n, &nested_mod, &inner_imports, items);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+/// Extracts the `BindType` (the ident argument of `#[metadata(...)]`).
+fn extract_bind_type(attrs: &[syn::Attribute]) -> Option<String> {
+ for attr in attrs {
+ let path_ident = attr.path().segments.last()?.ident.to_string();
+ if path_ident != "metadata" {
+ continue;
+ }
+ if let syn::Meta::List(meta_list) = &attr.meta {
+ for token in meta_list.tokens.clone().into_iter() {
+ if let proc_macro2::TokenTree::Ident(ident) = token {
+ return Some(ident.to_string());
+ }
+ }
+ }
+ }
+ None
+}
+
+/// Extracts the `DataType` (the function's return type path last segment).
+fn extract_data_type(f: &syn::ItemFn) -> Option<String> {
+ let syn::ReturnType::Type(_, ty) = &f.sig.output else {
+ return None;
+ };
+ match ty.as_ref() {
+ syn::Type::Path(type_path) => type_path.path.segments.last().map(|s| s.ident.to_string()),
+ _ => None,
+ }
+}
+
+/// Collect `use` imports from a list of top-level items.
+///
+/// Returns a map of `short_name → (module_path, short_name)`.
+fn collect_use_imports(items: &[syn::Item]) -> HashMap<String, (String, String)> {
+ let mut map = HashMap::new();
+ for item in items {
+ if let Item::Use(use_item) = item {
+ collect_from_use_tree(&use_item.tree, "", &mut map);
+ }
+ }
+ map
+}
+
+/// Recursively traverse a `UseTree` and collect named imports.
+fn collect_from_use_tree(
+ tree: &UseTree,
+ prefix: &str,
+ map: &mut HashMap<String, (String, String)>,
+) {
+ match tree {
+ UseTree::Name(name) => {
+ let module = prefix.to_string();
+ let alias = name.ident.to_string();
+ map.entry(alias).or_insert((module, name.ident.to_string()));
+ }
+ UseTree::Path(use_path) => {
+ let new_prefix = if prefix.is_empty() {
+ use_path.ident.to_string()
+ } else {
+ format!("{}::{}", prefix, use_path.ident)
+ };
+ collect_from_use_tree(&use_path.tree, &new_prefix, map);
+ }
+ UseTree::Rename(rename) => {
+ let module = prefix.to_string();
+ let alias = rename.ident.to_string();
+ map.entry(alias)
+ .or_insert((module, rename.ident.to_string()));
+ }
+ UseTree::Glob(_) => {}
+ UseTree::Group(group) => {
+ for item in &group.items {
+ collect_from_use_tree(item, prefix, map);
+ }
+ }
+ }
+}
diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs
index 7e7cbd5..8b13e52 100644
--- a/mingling_pathf/test/src/lib.rs
+++ b/mingling_pathf/test/src/lib.rs
@@ -387,3 +387,36 @@ fn test_dispatcher_clap_dispatch_tree() {
assert!(r2.contains("::sub::__internal_dispatcher_delete"));
assert!(r2.contains("::sub::__internal_dispatcher_helpcmd"));
}
+
+#[test]
+fn test_metadata_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir()
+ .unwrap()
+ .join("src/test_files/test_metadata.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ // Root: BindType + DataType pairs
+ "::EntryGreet1",
+ "::Description1",
+ "::EntryGreet2",
+ "::Description2",
+ "::EntryGreet3",
+ "::LocalType3",
+ "::EntryGreet4",
+ "::std::collections::HashMap",
+ "::EntryGreet5",
+ "::Qualified5",
+ // Sub: BindType + DataType pairs
+ "::sub::EntrySub1",
+ "::sub::SubType1",
+ "::sub::EntrySub2",
+ "::sub::SubType2",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {entry}");
+ }
+}
diff --git a/mingling_pathf/test/src/test_files/test_metadata.rs b/mingling_pathf/test/src/test_files/test_metadata.rs
new file mode 100644
index 0000000..52a2d4c
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_metadata.rs
@@ -0,0 +1,40 @@
+// Root-level metadata functions
+#[mingling::macros::metadata(EntryGreet1)]
+pub fn get_desc1() -> Description1 {
+ Description1 {}
+}
+
+#[metadata(EntryGreet2)]
+fn get_desc2() -> Description2 {
+ Description2 {}
+}
+
+// Local DataType (defined in-crate) + foreign DataType
+#[metadata(EntryGreet3)]
+pub fn get_desc3() -> LocalType3 {
+ LocalType3 {}
+}
+
+use std::collections::HashMap;
+
+#[metadata(EntryGreet4)]
+fn get_desc4() -> HashMap<String, String> {
+ HashMap::new()
+}
+
+#[metadata(EntryGreet5)]
+pub fn get_desc5() -> crate::fully::Qualified5 {
+ crate::fully::Qualified5 {}
+}
+
+pub mod sub {
+ #[mingling::macros::metadata(EntrySub1)]
+ pub fn get_sub1() -> SubType1 {
+ SubType1 {}
+ }
+
+ #[metadata(EntrySub2)]
+ fn get_sub2() -> SubType2 {
+ SubType2 {}
+ }
+}