aboutsummaryrefslogtreecommitdiff
path: root/mingling_pathf/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-29 14:12:24 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-29 14:12:24 +0800
commit2fa18e2190fb3c17892e13cb06d330e707dc05ec (patch)
tree9a8a64565ee98a84c64c8677951df94bf3aa7176 /mingling_pathf/src
parentfaae53e760743971c43800f6e6bc2fcbaec582b7 (diff)
feat(pathf): add dispatch tree config and pass feature to analyzer
Add `PathfinderConfig` struct to control dispatch tree extraction, and wire `use_dispatch_tree` through `DispatcherPattern`, `init_with_config`, and `analyze_and_build_type_mapping_for`. Expose config and wrapper from `mingling_core` under the `pathf` feature.
Diffstat (limited to 'mingling_pathf/src')
-rw-r--r--mingling_pathf/src/config.rs19
-rw-r--r--mingling_pathf/src/lib.rs3
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs99
-rw-r--r--mingling_pathf/src/patterns/dispatcher.rs157
-rw-r--r--mingling_pathf/src/type_mapping_builder.rs33
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/");