aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md8
-rw-r--r--docs/_zh_CN/pages/other/features.md20
-rw-r--r--docs/pages/other/features.md18
-rw-r--r--mingling_macros/src/chain.rs14
-rw-r--r--mingling_macros/src/completion.rs12
-rw-r--r--mingling_macros/src/help.rs33
-rw-r--r--mingling_macros/src/lib.rs31
-rw-r--r--mingling_macros/src/renderer.rs32
8 files changed, 139 insertions, 29 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 637b074..b074be6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,6 +66,14 @@
- `#[completion]`: Attribute parameter parsing changed from `Ident` to `TypePath`, supporting `#[completion(crate::EntryFine)]`
- Fixed code generation in `build_chain_arm`, `build_chain_exist_arm`, `build_renderer_entry`, `build_renderer_exist_entry`, `build_general_renderer_entry`, and completion entry: `Self::#variant` match arms now only take the last segment ident of the type path (e.g. `Self::EntryFine`), rather than concatenating the full path directly (which would generate invalid syntax like `Self::crate::EntryFine`), while `downcast::<T>()` and `type Previous = T` still use the full path to ensure correct type resolution
+6. **\[macros:register\]** Added compile-time duplicate variant detection for chain, renderer, help, and completion registrations. When two `#[chain]` (or `#[renderer]`, `#[help]`, `#[completion]`) functions register the same type variant, the compiler now emits a clear error at the registration site (e.g. `fn handle_state_prev1(_p: StatePrev1)`) instead of silently producing an unreachable match arm that only manifests as dead code in the generated `do_chain()`/`render()` dispatch.
+
+ Affected registration points:
+ - `register_chain` — checks `CHAINS` set for existing entries with the same variant
+ - `register_renderer` — checks `RENDERERS` set
+ - `help_attr` (via `#[help]`) + `register_help` — checks `HELP_REQUESTS`; `register_help` also serves as a public escape hatch for manual help registration, automatically skipping the duplicate check when the exact same entry was pre-inserted by `#[help]`
+ - `completion_attr` (via `#[completion]`) — checks `COMPLETIONS` set
+
#### Optimizations:
1. **\[core:flag\]** Refactored the `special_argument!` and `special_arguments!` macros to replace index‑based `while` loops with iterator `position` and `drain`, improving both performance and readability.
diff --git a/docs/_zh_CN/pages/other/features.md b/docs/_zh_CN/pages/other/features.md
index cdb2220..558720c 100644
--- a/docs/_zh_CN/pages/other/features.md
+++ b/docs/_zh_CN/pages/other/features.md
@@ -25,7 +25,7 @@ pack!(StateFoo = ());
#[chain]
async fn handle_state_foo(foo: StateFoo) -> Next {
StateFoo::new(())
-}
+}
```
详见 [示例](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-async-support)
@@ -92,14 +92,14 @@ fn main() {
例如,允许 `dispatcher!("greet")` 的缩写形式,自动生成 `CMDGreet` / `EntryGreet`。
-| 宏 | 说明 |
-|---|---|
-| `empty_result!()` | 链中提前返回空结果的简写 |
-| `entry!(Type, ["a", "b"])` | 构造入口类型的测试数据 |
-| `group!(Type)` | 将外部类型注册为组成员,无需修改其定义 |
-| `pack_err!(ErrorType)` / `pack_err!(ErrorType = Inner)` | 创建带自动 `name` 字段的错误类型 |
-| `#[program_setup]` | 声明程序初始化函数 |
-| `dispatcher!("cmd.path")` **缩写形式** | 省略 `CMDStruct => EntryStruct`,名字自动推导 |
+| 宏 | 说明 |
+| ------------------------------------------------------- | --------------------------------------------- |
+| `empty_result!()` | 链中提前返回空结果的简写 |
+| `entry!(Type, ["a", "b"])` | 构造入口类型的测试数据 |
+| `group!(Type)` | 将外部类型注册为组成员,无需修改其定义 |
+| `pack_err!(ErrorType)` / `pack_err!(ErrorType = Inner)` | 创建带自动 `name` 字段的错误类型 |
+| `#[program_setup]` | 声明程序初始化函数 |
+| `dispatcher!("cmd.path")` **缩写形式** | 省略 `CMDStruct => EntryStruct`,名字自动推导 |
<details>
<summary> Details </summary>
@@ -115,7 +115,7 @@ pack!(StatePrev2 = ());
pack!(StateNext = ());
#[chain]
-fn handle_state_prev2(_p: StatePrev1) {
+fn handle_state_prev2(_p: StatePrev2) {
// 无 Next 的 #[chain] 可以直接不返回值
}
diff --git a/docs/pages/other/features.md b/docs/pages/other/features.md
index 6e2a364..5ee0497 100644
--- a/docs/pages/other/features.md
+++ b/docs/pages/other/features.md
@@ -92,14 +92,14 @@ Enables an additional set of macros, providing more convenient syntactic sugar a
For example, allows the shorthand form `dispatcher!("greet")`, which auto-generates `CMDGreet` / `EntryGreet`.
-| Macro | Description |
-|---|---|
-| `empty_result!()` | Shorthand for returning an empty result early in a chain |
-| `entry!(Type, ["a", "b"])` | Construct test data for an entry type |
-| `group!(Type)` | Register external types as group members without modifying them |
-| `pack_err!(ErrorType)` / `pack_err!(ErrorType = Inner)` | Create error types with an automatic `name` field |
-| `#[program_setup]` | Declare a program initialization function |
-| `dispatcher!("cmd.path")` **shorthand** | Omit `CMDStruct => EntryStruct`, names are auto-derived |
+| Macro | Description |
+| ------------------------------------------------------- | --------------------------------------------------------------- |
+| `empty_result!()` | Shorthand for returning an empty result early in a chain |
+| `entry!(Type, ["a", "b"])` | Construct test data for an entry type |
+| `group!(Type)` | Register external types as group members without modifying them |
+| `pack_err!(ErrorType)` / `pack_err!(ErrorType = Inner)` | Create error types with an automatic `name` field |
+| `#[program_setup]` | Declare a program initialization function |
+| `dispatcher!("cmd.path")` **shorthand** | Omit `CMDStruct => EntryStruct`, names are auto-derived |
<details>
<summary> Details </summary>
@@ -115,7 +115,7 @@ pack!(StatePrev2 = ());
pack!(StateNext = ());
#[chain]
-fn handle_state_prev2(_p: StatePrev1) {
+fn handle_state_prev2(_p: StatePrev2) {
// A #[chain] with no return type can simply omit the return value
}
diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/chain.rs
index eaf43fb..a678858 100644
--- a/mingling_macros/src/chain.rs
+++ b/mingling_macros/src/chain.rs
@@ -432,6 +432,20 @@ pub fn register_chain(input: TokenStream) -> TokenStream {
let chain_entry_str = chain_entry.to_string();
let chain_exist_entry_str = chain_exist_entry.to_string();
+ // Check for duplicate variant before inserting
+ let variant_name = previous_type
+ .path
+ .segments
+ .last()
+ .unwrap()
+ .ident
+ .to_string();
+ if let Err(err) =
+ crate::check_duplicate_variant(&chains, &variant_name, "chain", previous_type.span())
+ {
+ return err.into();
+ }
+
chains.insert(chain_entry_str);
chain_exist.insert(chain_exist_entry_str);
diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/completion.rs
index 452c418..c4870d9 100644
--- a/mingling_macros/src/completion.rs
+++ b/mingling_macros/src/completion.rs
@@ -153,6 +153,18 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let mut completions = get_global_set(&crate::COMPLETIONS).lock().unwrap();
let completion_str = completion_entry.to_string();
+
+ // Check for duplicate variant before inserting
+ let variant_name = previous_type_ident.to_string();
+ if let Err(err) = crate::check_duplicate_variant(
+ &completions,
+ &variant_name,
+ "completion",
+ previous_type_path.span(),
+ ) {
+ return err.into();
+ }
+
completions.insert(completion_str);
expanded.into()
diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs
index e904b16..e1095e6 100644
--- a/mingling_macros/src/help.rs
+++ b/mingling_macros/src/help.rs
@@ -105,6 +105,18 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
// Register the help request mapping
let help_entry = build_help_entry(&struct_name, &entry_type);
let entry_str = help_entry.to_string();
+
+ // Check for duplicate variant before inserting
+ let variant_name = entry_type.path.segments.last().unwrap().ident.to_string();
+ {
+ let helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap();
+ if let Err(err) =
+ crate::check_duplicate_variant(&helps, &variant_name, "help", entry_type.span())
+ {
+ return err.into();
+ }
+ }
+
get_global_set(&crate::HELP_REQUESTS)
.lock()
.unwrap()
@@ -185,10 +197,23 @@ pub fn register_help(input: TokenStream) -> TokenStream {
// Register the help request mapping
let help_entry = build_help_entry(&struct_name, &entry_type);
let entry_str = help_entry.to_string();
- get_global_set(&crate::HELP_REQUESTS)
- .lock()
- .unwrap()
- .insert(entry_str);
+
+ // Check if entry was already pre-inserted by `#[help]` attribute
+ let mut helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap();
+ if helps.contains(&entry_str) {
+ // Already registered by `#[help]`, no duplicate check needed
+ return quote! {}.into();
+ }
+
+ // Check for duplicate variant (different struct, same type)
+ let variant_name = entry_type.path.segments.last().unwrap().ident.to_string();
+ if let Err(err) =
+ crate::check_duplicate_variant(&helps, &variant_name, "help", entry_type.span())
+ {
+ return err.into();
+ }
+
+ helps.insert(entry_str);
quote! {}.into()
}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index c6b94c3..5bc73a4 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -195,6 +195,37 @@ pub(crate) static CHAINS_EXIST: Registry = OnceLock::new();
pub(crate) static RENDERERS_EXIST: Registry = OnceLock::new();
pub(crate) static HELP_REQUESTS: Registry = OnceLock::new();
+/// Checks if a variant name already exists in a registered set.
+/// Returns a `compile_error` token stream if a duplicate is found.
+pub(crate) fn check_duplicate_variant(
+ set: &std::collections::BTreeSet<String>,
+ variant_name: &str,
+ kind: &str,
+ error_span: proc_macro2::Span,
+) -> Result<(), proc_macro2::TokenStream> {
+ for existing in set.iter() {
+ if entry_has_variant(existing, variant_name) {
+ return Err(syn::Error::new(
+ error_span,
+ format!(
+ "duplicate {kind} registration for `{variant_name}`: a {kind} with this type already exists"
+ ),
+ )
+ .to_compile_error());
+ }
+ }
+ Ok(())
+}
+
+/// Checks if a stored entry string contains the given variant name.
+/// Handles both "StructName => Variant," and "Self::Variant => ..." formats.
+fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
+ entry.contains(&format!("=> {variant_name},"))
+ || entry.contains(&format!("=> {variant_name} "))
+ || entry.contains(&format!("=> {variant_name}"))
+ || entry.contains(&format!(":: {variant_name} =>"))
+}
+
/// Registers an outside-type as a member of a program group without modifying its definition.
///
/// This macro allows you to use outside-types from external crates (like `std::io::Error`)
diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/renderer.rs
index c4b2786..79128ef 100644
--- a/mingling_macros/src/renderer.rs
+++ b/mingling_macros/src/renderer.rs
@@ -234,18 +234,38 @@ pub fn register_renderer(input: TokenStream) -> TokenStream {
#[cfg(feature = "general_renderer")]
let general_renderer_entry = build_general_renderer_entry(&previous_type);
- let mut renderers = get_global_set(&crate::RENDERERS).lock().unwrap();
- let mut renderer_exist = get_global_set(&crate::RENDERERS_EXIST).lock().unwrap();
-
- #[cfg(feature = "general_renderer")]
- let mut general_renderers = get_global_set(&crate::GENERAL_RENDERERS).lock().unwrap();
-
let renderer_entry_str = renderer_entry.to_string();
let renderer_exist_entry_str = renderer_exist_entry.to_string();
#[cfg(feature = "general_renderer")]
let general_renderer_entry_str = general_renderer_entry.to_string();
+ // Check for duplicate variant before acquiring other locks
+ let variant_name = previous_type
+ .path
+ .segments
+ .last()
+ .unwrap()
+ .ident
+ .to_string();
+ {
+ let renderers = get_global_set(&crate::RENDERERS).lock().unwrap();
+ if let Err(err) = crate::check_duplicate_variant(
+ &renderers,
+ &variant_name,
+ "renderer",
+ previous_type.span(),
+ ) {
+ return err.into();
+ }
+ } // renderers lock released here
+
+ let mut renderers = get_global_set(&crate::RENDERERS).lock().unwrap();
+ let mut renderer_exist = get_global_set(&crate::RENDERERS_EXIST).lock().unwrap();
+
+ #[cfg(feature = "general_renderer")]
+ let mut general_renderers = get_global_set(&crate::GENERAL_RENDERERS).lock().unwrap();
+
renderers.insert(renderer_entry_str);
renderer_exist.insert(renderer_exist_entry_str);