diff options
Diffstat (limited to 'mingling_pathf')
| -rw-r--r-- | mingling_pathf/src/pattern_analyzer.rs | 1 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns.rs | 2 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/metadata.rs | 165 | ||||
| -rw-r--r-- | mingling_pathf/test/src/lib.rs | 33 | ||||
| -rw-r--r-- | mingling_pathf/test/src/test_files/test_metadata.rs | 40 |
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 {} + } +} |
