delegate_match/lib.rs
1#![doc = include_str!("../README.md")]
2// Deny lints.
3#![deny(
4 unsafe_code,
5 reason = "this crate is not responsible for anything that requires `unsafe`"
6)]
7#![deny(
8 clippy::unwrap_used,
9 reason = "using `expect` instead shows intent better"
10)]
11#![deny(nonstandard_style, reason = "use commonly agreed on standards")]
12#![deny(future_incompatible, reason = "provides easier maintenance")]
13#![deny(deprecated_safe, reason = "better safety guarantees")]
14// Warn lints.
15#![warn(missing_docs, reason = "helps with documentation coverage")]
16#![warn(clippy::cargo, reason = "improves crate metadata quality")]
17#![warn(missing_debug_implementations, reason = "this is a library")]
18#![warn(
19 clippy::pedantic,
20 reason = "useful but conservative lints - use #[allow] attributes on false positives"
21)]
22#![warn(
23 clippy::nursery,
24 reason = "experimental lints, use #[allow] to disable annoying ones"
25)]
26
27mod delegate_arm;
28mod delegate_entry;
29mod delegate_match;
30mod expr;
31mod substitute;
32mod util;
33
34use proc_macro::TokenStream;
35use quote::ToTokens as _;
36use syn::parse_macro_input;
37
38/// Convenience macro for writing grouped `match` arms for different underlying types.
39///
40/// Writing repetitive `match` arms for enumerations (or other pattern-matching
41/// constructs) — especially when types, but not the API, differ —
42/// can quickly become boilerplate. `delegate_match!` lets you list
43/// several patterns up-front once and then re-uses a single body for each
44/// of them, automatically expanding into equivalent ordinary Rust code.
45///
46/// ## Syntax outline
47///
48/// ```text
49/// match <scrutinee_expr> {
50/// [<arm_path>::]{ <entry_pat> [: <assoc_ts>][, ...] } [<arm_pat>] [if <guard_expr>] => <body_expr>[[,] ...]
51/// }
52/// ```
53///
54/// - `arm_path` — optional path prefix (e.g. `MyEnum` or `::std::io`)
55/// - `entry_pat` — individual *entry pattern*, also available as the `$entry_pat` placeholder.
56/// - `assoc_ts` — *associated tokens*, also available as the `$assoc_ts` placeholder.
57/// - `arm_pat` — an optional pattern appended to every entry.
58/// - `guard_expr` — an optional `if` guard.
59/// - `body_expr` — expression generated for each entry.
60///
61/// This expands to:
62///
63/// ```text
64/// match <scrutinee_expr> {
65/// <arm_path>::<entry_pat><arm_pat> if <guard_expr> => <body_expr>
66/// }
67/// ```
68///
69/// Two placeholders are substituted for every entry *before the code is
70/// type-checked*, and they may appear in the following places:
71/// - inside the delegate arm pattern `arm_pat` (if present),
72/// - inside the match arm guard expression `guard_expr` (if present),
73/// - inside the arm body expression `body_expr`.
74///
75/// The available placeholders are:
76/// - `$entry_pat` — the entry pattern for a generated arm.
77/// - `$assoc_ts` — the tokens following an entry, up until the next one (excluding the colon).
78///
79/// The macro is supposed to accept standard Rust `match` expression syntax, extended with the above.
80/// Any other deviation should generally be considered a bug.
81///
82/// ## Semantics
83///
84/// - For each *entry* in a grouped arm, the macro generates a regular match arm.
85/// - Everything else (outer attributes, guards, commas...) is preserved exactly as you write it.
86/// - The only exception to that is placeholder substitution.
87/// - This macro performs generation before type-checking is done, so
88/// the generated code is capable of working with different types, if constructed appropriately.
89/// - The order of generated arms is the order of entries in the source code.
90///
91/// ## Examples
92///
93/// ### Delegating to the same code for multiple enum variants
94///
95/// ```rust
96/// use delegate_match::delegate_match;
97///
98/// enum MouseEvent { Scroll(i16, i16), Position(i32, i32) }
99/// let ev = MouseEvent::Scroll(10, 20);
100///
101/// delegate_match! {
102/// match ev {
103/// // This expands to two individual arms.
104/// MouseEvent::{ Scroll, Position }(x, y) => {
105/// println!("mouse event: $entry_pat → ({x}, {y})")
106/// }
107/// }
108/// }
109/// ```
110///
111/// ### Using placeholders
112///
113/// ```rust
114/// # use delegate_match::delegate_match;
115/// # enum Msg { Ping, Log }
116/// # let msg = Msg::Log;
117/// delegate_match! {
118/// match msg {
119/// // `$assoc_ts` and `$entry_pat` are substituted at compile time.
120/// Msg::{ Ping: "🏓", Log: "📝" } => {
121/// // Outputs "🏓 Ping" or "📝 Log" depending on the variant.
122/// println!("{} {}", $assoc_ts, stringify!($entry_pat))
123/// }
124/// }
125/// }
126/// ```
127///
128/// ### Adding an if guard to multiple entries
129///
130/// ```rust
131/// # use delegate_match::delegate_match;
132/// # enum Number { I32(i32), I64(i64) }
133/// # let n = Number::I32(4);
134/// delegate_match! {
135/// match n {
136/// // This works despite `val` being of different types for each variant.
137/// // This is because a separate arm is generated for each entry!
138/// Number::{ I32, I64 }(val) if val % 2 == 0 => {
139/// println!("even {}", val)
140/// }
141/// // We must also account for the rest of the cases.
142/// Number::{ I32, I64 }(val) => {
143/// println!("odd {}", val)
144/// }
145/// }
146/// }
147/// ```
148#[proc_macro_error2::proc_macro_error]
149#[proc_macro]
150pub fn delegate_match(input: TokenStream) -> TokenStream {
151 let parsed = parse_macro_input!(input as delegate_match::ExprDelegateMatch);
152 parsed.into_token_stream().into()
153}
154
155#[cfg(test)]
156#[test]
157fn trybuild_tests_compile_fail() {
158 let t = trybuild::TestCases::new();
159 t.compile_fail("tests/compile_fail/*.rs");
160}