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 associated;
28mod delegate_arm;
29mod delegate_entry;
30mod delegate_match;
31mod expr;
32mod substitute;
33mod util;
34
35use proc_macro::TokenStream;
36use quote::ToTokens as _;
37use syn::parse_macro_input;
38
39/// Convenience macro for writing grouped `match` arms for different underlying types.
40///
41/// Writing repetitive `match` arms for enumerations (or other pattern-matching
42/// constructs) — especially when types, but not the API, differ —
43/// can quickly become boilerplate. `delegate_match!` lets you list
44/// several patterns up-front once and then re-uses a single body for each
45/// of them, automatically expanding into equivalent ordinary Rust code.
46///
47/// ## Syntax outline
48///
49/// ```text
50/// match <scrutinee_expr> {
51///     [<arm_path>::]{ <entry_pat> [: <assoc_ts>][, ...] } [<arm_pat>] [if <guard_expr>] => <body_expr>[[,] ...]
52/// }
53/// ```
54///
55/// - `arm_path` &mdash; optional path prefix (e.g. `MyEnum` or `::std::io`)
56/// - `entry_pat` &mdash; individual *entry pattern*, also available as the `$entry_pat` placeholder.
57/// - `assoc_ts` &mdash; *associated syntax item*, also available as the `$assoc_ts` placeholder.
58/// - `arm_pat` &mdash; an optional pattern appended to every entry.
59/// - `guard_expr` &mdash; an optional `if` guard.
60/// - `body_expr` &mdash; expression generated for each entry.
61///
62/// This expands to:
63///
64/// ```text
65/// match <scrutinee_expr> {
66///     <arm_path>::<entry_pat><arm_pat> if <guard_expr> => <body_expr>
67/// }
68/// ```
69///
70/// Two placeholders are substituted for every entry *before the code is
71/// type-checked*, and they may appear in the following places:
72///   - inside the delegate arm pattern `arm_pat` (if present),
73///   - inside the match arm guard expression `guard_expr` (if present),
74///   - inside the arm body expression `body_expr`.
75///
76/// The available placeholders are:
77///   - `$entry_pat` &mdash; the entry pattern for a generated arm.
78///   - `$assoc_ts` &mdash; the tokens following an entry, up until the next one (excluding the colon).
79///     Can be any of the following:
80///       - Expression
81///       - Statement
82///       - Pattern
83///       - Type
84///     The first that matches is used, in the above order.
85///
86/// The macro is supposed to accept standard Rust `match` expression syntax, extended with the above.
87/// Any other deviation should generally be considered a bug.
88///
89/// ## Semantics
90///
91/// - For each *entry* in a grouped arm, the macro generates a regular match arm.
92/// - Everything else (outer attributes, guards, commas...) is preserved exactly as you write it.
93/// - The only exception to that is placeholder substitution.
94/// - This macro performs generation before type-checking is done, so
95///   the generated code is capable of working with different types, if constructed appropriately.
96/// - The order of generated arms is the order of entries in the source code.
97///
98/// ## Examples
99///
100/// ### Delegating to the same code for multiple enum variants
101///
102/// ```rust
103/// use delegate_match::delegate_match;
104///
105/// enum MouseEvent { Scroll(i16, i16), Position(i32, i32) }
106/// let ev = MouseEvent::Scroll(10, 20);
107///
108/// delegate_match! {
109///     match ev {
110///         // This expands to two individual arms.
111///         MouseEvent::{ Scroll, Position }(x, y) => {
112///             println!("mouse event: $entry_pat -> ({x}, {y})")
113///         }
114///     }
115/// }
116/// ```
117///
118/// ### Using placeholders
119///
120/// ```rust
121/// # use delegate_match::delegate_match;
122/// # enum Msg { Ping, Log }
123/// # let msg = Msg::Log;
124/// delegate_match! {
125///     match msg {
126///         // `$assoc_ts` and `$entry_pat` are substituted at compile time.
127///         Msg::{ Ping: "🏓", Log: "📝" } => {
128///             // Outputs "🏓 Ping" or "📝 Log" depending on the variant.
129///             println!("{} {}", $assoc_ts, stringify!($entry_pat))
130///         }
131///     }
132/// }
133/// ```
134///
135/// ### Adding an if guard to multiple entries
136///
137/// ```rust
138/// # use delegate_match::delegate_match;
139/// # enum Number { I32(i32), I64(i64) }
140/// # let n = Number::I32(4);
141/// delegate_match! {
142///     match n {
143///         // This works despite `val` being of different types for each variant.
144///         // This is because a separate arm is generated for each entry!
145///         Number::{ I32, I64 }(val) if val % 2 == 0 => {
146///             println!("even {}", val)
147///         }
148///         // We must also account for the rest of the cases.
149///         Number::{ I32, I64 }(val) => {
150///             println!("odd {}", val)
151///         }
152///     }
153/// }
154/// ```
155#[proc_macro_error2::proc_macro_error]
156#[proc_macro]
157pub fn delegate_match(input: TokenStream) -> TokenStream {
158    let parsed = parse_macro_input!(input as delegate_match::ExprDelegateMatch);
159    parsed.into_token_stream().into()
160}
161
162#[cfg(test)]
163#[test]
164fn trybuild_tests_compile_fail() {
165    let t = trybuild::TestCases::new();
166    t.compile_fail("tests/compile_fail/*.rs");
167}