diff options
Diffstat (limited to 'mingling_pathf/src')
| -rw-r--r-- | mingling_pathf/src/config.rs | 19 | ||||
| -rw-r--r-- | mingling_pathf/src/lib.rs | 3 | ||||
| -rw-r--r-- | mingling_pathf/src/pattern_analyzer.rs | 99 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/dispatcher.rs | 157 | ||||
| -rw-r--r-- | mingling_pathf/src/type_mapping_builder.rs | 33 |
5 files changed, 191 insertions, 120 deletions
diff --git a/mingling_pathf/src/config.rs b/mingling_pathf/src/config.rs new file mode 100644 index 0000000..6758264 --- /dev/null +++ b/mingling_pathf/src/config.rs @@ -0,0 +1,19 @@ +/// Configuration for the module pathfinder analysis. +/// +/// Controls behavior such as whether dispatch-tree related types +/// (`__internal_dispatcher_*`) should be extracted. +#[derive(Debug, Clone, Default)] +pub struct PathfinderConfig { + /// Whether to also extract `__internal_dispatcher_*` static types + /// generated by the `dispatch_tree` feature in Mingling. + pub use_dispatch_tree: bool, +} + +impl PathfinderConfig { + /// Create a config with `use_dispatch_tree` enabled. + pub fn with_dispatch_tree() -> Self { + Self { + use_dispatch_tree: true, + } + } +} diff --git a/mingling_pathf/src/lib.rs b/mingling_pathf/src/lib.rs index 40fd2ec..81b2df6 100644 --- a/mingling_pathf/src/lib.rs +++ b/mingling_pathf/src/lib.rs @@ -1,6 +1,7 @@ +pub mod config; +pub mod error; pub mod module_pathf; pub mod pattern_analyzer; -pub mod error; pub mod patterns; mod type_mapping_builder; diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index 4a1f8a4..bfc2dc3 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -1,32 +1,29 @@ use std::collections::HashSet; use std::path::Path; +use crate::config::PathfinderConfig; use crate::error::MinglingPathfinderError; +use crate::patterns::*; -/// Top-level convenience function: creates a default PatternAnalyzer with all built-in patterns pre-registered +/// Creates a default `PatternAnalyzer` with all built-in patterns pre-registered. pub fn init() -> PatternAnalyzer { - let mut analyzer = PatternAnalyzer::new(); - macro_rules! __register { - ( $($pattern:ident),* $(,)? ) => { - $( - analyzer.add_pattern(crate::patterns::$pattern); - )* - }; - } - - __register![ - BasicStructPattern, - PackPattern, - GroupPattern, - GrouppedDerivePattern, - ChainPattern, - RendererPattern, - HelpPattern, - CompletionPattern, - DispatcherPattern, - DispatcherClapPattern, - ]; + init_with_config(PathfinderConfig::default()) +} +/// Creates a `PatternAnalyzer` with the given config, used by `mingling_core`'s pathf wrapper +/// to inject feature-dependent settings (e.g., `dispatch_tree`). +pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer { + let mut analyzer = PatternAnalyzer::new(); + analyzer.add_pattern(BasicStructPattern); + analyzer.add_pattern(PackPattern); + analyzer.add_pattern(GroupPattern); + analyzer.add_pattern(GrouppedDerivePattern); + analyzer.add_pattern(ChainPattern); + analyzer.add_pattern(RendererPattern); + analyzer.add_pattern(HelpPattern); + analyzer.add_pattern(CompletionPattern); + analyzer.add_pattern(DispatcherPattern::new(config.use_dispatch_tree)); + analyzer.add_pattern(DispatcherClapPattern); analyzer } @@ -83,71 +80,41 @@ pub trait AnalyzePattern { /// A pattern analyzer that registers and runs multiple `AnalyzePattern` instances to parse /// referenceable items from code. -/// -/// Internally maintains a `Vec<Box<dyn AnalyzePattern>>` for dynamic dispatch, supporting -/// runtime registration of custom patterns. The `Default` derive provides an empty analyzer -/// instance. -/// -/// # Fields -/// - `patterns`: A list of registered analysis patterns, each responsible for detecting and -/// extracting a specific type of analyzable item (e.g., structs, functions). #[derive(Default)] pub struct PatternAnalyzer { - /// A list of registered analysis patterns, each responsible for detecting and extracting - /// a specific type of analyzable item. patterns: Vec<Box<dyn AnalyzePattern>>, } impl PatternAnalyzer { - /// Creates a new `PatternAnalyzer` instance. - /// - /// Internally calls `Default::default()` directly, initially containing no registered patterns. pub fn new() -> Self { Self::default() } - /// Registers an analysis pattern with the current analyzer. - /// - /// The pattern is wrapped in a `Box` and stored in the internal pattern list, - /// and will be used to scan and analyze file content in subsequent calls to `analyze_file`. - /// - /// # Parameters - /// - `pattern` —— A type instance implementing the `AnalyzePattern` trait, - /// responsible for detecting and extracting a specific syntactic structure (e.g., structs, functions). pub fn add_pattern(&mut self, pattern: impl AnalyzePattern + 'static) { self.patterns.push(Box::new(pattern)); } /// Analyzes a single file and returns a formatted set of strings. - /// - /// This method reads the content of the file at the specified path, then uses each registered - /// pattern to analyze the content in turn. Only patterns whose `contains` method returns `true` - /// will trigger the subsequent `analyze` call. All extracted `AnalyzeItem`s are merged and - /// converted into a formatted `HashSet<String>`. - /// - /// # Parameters - /// - `path` —— The path to the file to analyze, supporting any type that implements `AsRef<Path>`. - /// - /// # Returns - /// - `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() - }) + 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> { + 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> { + 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)?; diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs index c347351..b9f147d 100644 --- a/mingling_pathf/src/patterns/dispatcher.rs +++ b/mingling_pathf/src/patterns/dispatcher.rs @@ -2,14 +2,24 @@ use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; -/// Matches the `dispatcher!` macro, extracts the entry type name. -/// +/// Matches the `dispatcher!` macro, extracts: +/// - `Entry*` — the entry type (always) +/// - `CMD*` — the dispatcher struct (always) +/// - `__internal_dispatcher_*` — dispatch tree static (when `use_dispatch_tree` is true) +pub struct DispatcherPattern { + pub use_dispatch_tree: bool, +} + +impl DispatcherPattern { + pub fn new(use_dispatch_tree: bool) -> Self { + Self { use_dispatch_tree } + } +} + /// Supported forms: /// - `dispatcher!("greet", CMDGreet => EntryGreet)` — explicit -/// - `dispatcher!("greet")` — implicit, infers EntryType +/// - `dispatcher!("greet")` — implicit, infers names /// - `dispatcher! { ... }` — with braces -pub struct DispatcherPattern; - impl AnalyzePattern for DispatcherPattern { fn contains(&self, content: &str) -> bool { content.contains("dispatcher!") @@ -29,12 +39,7 @@ impl AnalyzePattern for DispatcherPattern { 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, - }); - } + items.extend(extract_all_types(&m.mac.tokens, "", self.use_dispatch_tree)); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { @@ -43,12 +48,11 @@ impl AnalyzePattern for DispatcherPattern { 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.extend(extract_all_types( + &m.mac.tokens, + &item_mod.ident.to_string(), + self.use_dispatch_tree, + )); } } } @@ -70,35 +74,105 @@ fn macro_simple_name(m: &syn::ItemMacro) -> 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> { +/// Extracts all types generated by a `dispatcher!` call. +fn extract_all_types( + tokens: &proc_macro2::TokenStream, + module: &str, + use_dispatch_tree: bool, +) -> Vec<AnalyzeItem> { + let (cmd_name, cmd_struct, entry_struct) = parse_dispatcher_args(tokens); + let cmd_name = match cmd_name { + Some(n) => n, + None => return Vec::new(), + }; + + let mut items = Vec::new(); + + // Entry type — always + if let Some(ref entry) = entry_struct { + items.push(AnalyzeItem { + module: module.to_string(), + item_name: entry.clone(), + }); + } + + // CMD type — always + if let Some(ref cmd) = cmd_struct { + items.push(AnalyzeItem { + module: module.to_string(), + item_name: cmd.clone(), + }); + } + + // __internal_dispatcher_* — when configured + if use_dispatch_tree { + let internal_name = format!("__internal_dispatcher_{}", snake_case(&cmd_name)); + items.push(AnalyzeItem { + module: module.to_string(), + item_name: internal_name, + }); + } + + items +} + +/// Parses dispatcher arguments and returns (command_name, cmd_struct, entry_struct). +fn parse_dispatcher_args( + tokens: &proc_macro2::TokenStream, +) -> (Option<String>, Option<String>, Option<String>) { let stream = tokens.to_string(); - // Explicit form: look for `=>` + // Explicit form: "name", CMDType => EntryType if let Some(arrow_idx) = stream.find("=>") { + // Extract command name + let before_arrow = &stream[..arrow_idx]; + let cmd_name = extract_string_literal(before_arrow); + + // Extract CMD type: the ident before `=>` + let before_arrow_trimmed = before_arrow.trim(); + let cmd_type = before_arrow_trimmed + .split(|c: char| c.is_whitespace() || c == ',') + .filter_map(|s| { + let s = s.trim(); + if s.starts_with('"') || s.is_empty() { + None + } else { + Some(s.to_string()) + } + }) + .next_back(); + + // Extract entry type: after `=>` let after_arrow = stream[arrow_idx + 2..].trim(); - let entry_name = after_arrow + let entry_type = after_arrow .split(|c: char| c.is_whitespace() || c == ',' || c == ')' || c == '}') - .next()?; - if !entry_name.is_empty() { - return Some(entry_name.trim().to_string()); - } - } + .next() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); - // 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 entry = format!("Entry{}", to_pascal_case(cmd_name)); - Some(entry) - } else { - None + return (cmd_name, cmd_type, entry_type); } + + // Implicit form: "name" + let cmd_name = match extract_string_literal(&stream) { + Some(n) => n, + None => return (None, None, None), + }; + let pascal = to_pascal_case(&cmd_name); + ( + Some(cmd_name), + Some(format!("CMD{pascal}")), + Some(format!("Entry{pascal}")), + ) +} + +/// Extracts the first string literal from a token string. +fn extract_string_literal(s: &str) -> Option<String> { + let s = s.trim(); + let start = s.find('"')?; + let rest = &s[start + 1..]; + let end = rest.find('"')?; + Some(rest[..end].to_string()) } fn to_pascal_case(s: &str) -> String { @@ -113,3 +187,8 @@ fn to_pascal_case(s: &str) -> String { }) .collect() } + +/// Simple snake_case conversion (replaces `.`, `-` with `_`). +fn snake_case(s: &str) -> String { + s.replace(['.', '-'], "_").to_lowercase() +} diff --git a/mingling_pathf/src/type_mapping_builder.rs b/mingling_pathf/src/type_mapping_builder.rs index c701536..3422af8 100644 --- a/mingling_pathf/src/type_mapping_builder.rs +++ b/mingling_pathf/src/type_mapping_builder.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; -use std::path::{Path}; +use std::path::Path; +use crate::config::PathfinderConfig; use crate::error::MinglingPathfinderError; use crate::module_pathf; use crate::pattern_analyzer; @@ -9,14 +10,16 @@ use crate::pattern_analyzer; /// /// `crate_dir` — crate root directory (i.e., the directory containing Cargo.toml) /// `output_dir` — directory where mapping files will be written +/// `config` — pathfinder configuration (e.g., dispatch_tree detection) /// /// Mapping file format per line: `TypeName = crate::module::path::TypeName` pub fn analyze_and_build_type_mapping_for( crate_dir: &Path, output_dir: &Path, + config: &PathfinderConfig, ) -> Result<(), MinglingPathfinderError> { let module_mapping = module_pathf::analyze(crate_dir)?; - let analyzer = pattern_analyzer::init(); + let analyzer = pattern_analyzer::init_with_config(config.clone()); let mut type_mappings: Vec<(String, String)> = Vec::new(); @@ -75,22 +78,24 @@ pub fn analyze_and_build_type_mapping_for( /// /// 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_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)?; + analyze_and_build_type_mapping_for(&crate_dir, &output_dir, &PathfinderConfig::default())?; // Notify Cargo to re-run build.rs when source files change println!("cargo:rerun-if-changed=src/"); |
