Skip to main content

format_attr/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(docsrs, allow(unused_attributes))]
3#![doc = include_str!("../README.md")]
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{parse::Parse, punctuated::Punctuated, DeriveInput, Expr, LitStr, Token};
8
9/// Attribute arguments for `#[fmt("...", arg1, arg2)]`
10struct FmtArgs {
11    format_str: LitStr,
12    _comma: Option<Token![,]>,
13    args: Punctuated<Expr, Token![,]>,
14}
15
16impl Parse for FmtArgs {
17    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
18        let format_str: LitStr = input.parse()?;
19        let _comma: Option<Token![,]> = input.parse()?;
20        let args: Punctuated<Expr, Token![,]> =
21            input.parse_terminated(Expr::parse, Token![,])?;
22
23        Ok(FmtArgs {
24            format_str,
25            _comma,
26            args,
27        })
28    }
29}
30
31/// Parse a specific attribute from the struct attributes by name.
32fn parse_specific_attr(attrs: &[syn::Attribute], name: &str) -> Option<FmtArgs> {
33    for attr in attrs {
34        if attr.path().is_ident(name) {
35            return attr.parse_args().ok();
36        }
37    }
38    None
39}
40
41/// Parse `fmt_display` attribute, fallback to `fmt`.
42fn parse_display_attr(attrs: &[syn::Attribute]) -> Option<FmtArgs> {
43    parse_specific_attr(attrs, "fmt_display")
44        .or_else(|| parse_specific_attr(attrs, "fmt"))
45}
46
47/// Parse `fmt_debug` attribute, fallback to `fmt`.
48fn parse_debug_attr(attrs: &[syn::Attribute]) -> Option<FmtArgs> {
49    parse_specific_attr(attrs, "fmt_debug")
50        .or_else(|| parse_specific_attr(attrs, "fmt"))
51}
52
53/// Check if an expression is a simple field identifier (not `self.x` or method calls).
54/// Returns true if the expr is a simple identifier like `field_name`.
55fn is_simple_field(expr: &Expr) -> bool {
56    matches!(expr, Expr::Path(expr_path) if expr_path.path.get_ident().is_some())
57}
58
59/// Regex pattern to match `{field_name}` where field_name is a valid identifier.
60/// Matches: {name}, {name:?}, {name:#?}, {name:width}, {name:>10}
61/// Does NOT match: {}, {:?}, {0}, {0:?} (positional/numeric indices)
62fn extract_field_from_format(spec: &str) -> Option<&str> {
63    // Find the content inside braces
64    let content = spec.strip_prefix('{')?.strip_suffix('}')?;
65
66    // Split on ':' to separate field name from format spec
67    let field_part = content.split(':').next()?;
68
69    // Check if it's a valid identifier (starts with letter/underscore, contains only alphanumeric/underscore)
70    if field_part.is_empty() {
71        return None;
72    }
73
74    let first_char = field_part.chars().next()?;
75    if !first_char.is_ascii_alphabetic() && first_char != '_' {
76        return None;
77    }
78
79    if !field_part.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
80        return None;
81    }
82
83    Some(field_part)
84}
85
86/// Transform format string with `{field}` syntax to standard format string.
87/// Returns the transformed format string and a list of field identifiers to use as arguments.
88fn transform_format_string(input: &str) -> (String, Vec<String>) {
89    let mut result = String::with_capacity(input.len());
90    let mut fields = Vec::new();
91    let mut chars = input.chars().peekable();
92
93    while let Some(ch) = chars.next() {
94        if ch == '{' {
95            // Check if it's an escaped brace {{ or }}
96            if chars.peek() == Some(&'{') {
97                chars.next(); // consume second {
98                result.push_str("{{");
99                continue;
100            }
101
102            // Collect the content until }
103            let mut brace_content = String::new();
104            let mut found_closing = false;
105
106            for inner_ch in chars.by_ref() {
107                if inner_ch == '}' {
108                    found_closing = true;
109                    break;
110                }
111                brace_content.push(inner_ch);
112            }
113
114            if !found_closing {
115                // Unclosed brace, treat as literal
116                result.push('{');
117                result.push_str(&brace_content);
118                continue;
119            }
120
121            // Check if this is a named field reference
122            if let Some(field_name) = extract_field_from_format(&format!("{{{}}}", brace_content)) {
123                // It's a field name - replace with {} and add field to list
124                fields.push(field_name.to_string());
125
126                // Preserve the format spec after the field name
127                let format_spec = &brace_content[field_name.len()..];
128                result.push('{');
129                result.push_str(format_spec);
130                result.push('}');
131            } else {
132                // Not a field name (might be positional arg or empty), keep as-is
133                result.push('{');
134                result.push_str(&brace_content);
135                result.push('}');
136            }
137        } else if ch == '}' {
138            // Check if it's an escaped brace }}
139            if chars.peek() == Some(&'}') {
140                chars.next(); // consume second }
141                result.push_str("}}");
142            } else {
143                result.push('}');
144            }
145        } else {
146            result.push(ch);
147        }
148    }
149
150    (result, fields)
151}
152
153/// Generate the fmt implementation body.
154/// Automatically adds `self.` prefix to simple field identifiers.
155fn generate_fmt_body(fmt_args: &FmtArgs) -> proc_macro2::TokenStream {
156    let original_format_str = fmt_args.format_str.value();
157
158    // Transform {field} syntax in format string
159    let (transformed_format, extracted_fields) = transform_format_string(&original_format_str);
160
161    // Build the final format string literal
162    let format_str = LitStr::new(&transformed_format, fmt_args.format_str.span());
163
164    // Collect arguments: first from extracted fields in format string, then from explicit args
165    let mut all_args: Vec<proc_macro2::TokenStream> = Vec::new();
166
167    // Add extracted fields from {field} syntax (as self.field)
168    for field in &extracted_fields {
169        let field_ident = syn::Ident::new(field, proc_macro2::Span::call_site());
170        all_args.push(quote! { self.#field_ident });
171    }
172
173    // Add explicit arguments (with self. prefix for simple fields)
174    for arg in &fmt_args.args {
175        if is_simple_field(arg) {
176            // Simple field identifier: add self. prefix
177            all_args.push(quote! { self.#arg });
178        } else {
179            // Complex expression: keep as-is
180            all_args.push(quote! { #arg });
181        }
182    }
183
184    quote! {
185        write!(f, #format_str #(, #all_args)*)
186    }
187}
188
189/// Derive macro for implementing `std::fmt::Display`.
190///
191/// # Attributes
192///
193/// - `#[fmt_display("...", args...)]` - Format string specifically for `Display` (highest priority)
194/// - `#[fmt("...", args...)]` - Shared format string for both `Display` and `Debug` (fallback)
195///
196/// # Example
197///
198/// ```
199/// use format_attr::DisplayAttr;
200///
201/// #[derive(DisplayAttr)]
202/// #[fmt("User: {}", self.name)]
203/// struct User {
204///     name: String,
205/// }
206/// ```
207#[proc_macro_derive(DisplayAttr, attributes(fmt, fmt_display))]
208pub fn derive_display(input: TokenStream) -> TokenStream {
209    let input = syn::parse_macro_input!(input as DeriveInput);
210
211    let name = &input.ident;
212    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
213
214    let fmt_args = match parse_display_attr(&input.attrs) {
215        Some(args) => args,
216        None => {
217            return syn::Error::new_spanned(
218                &input,
219                "DisplayAttr requires a #[fmt(...)] or #[fmt_display(...)] attribute",
220            )
221            .to_compile_error()
222            .into();
223        }
224    };
225
226    let fmt_body = generate_fmt_body(&fmt_args);
227
228    let expanded = quote! {
229        impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
230            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231                #fmt_body
232            }
233        }
234    };
235
236    TokenStream::from(expanded)
237}
238
239/// Derive macro for implementing `std::fmt::Debug`.
240///
241/// # Attributes
242///
243/// - `#[fmt_debug("...", args...)]` - Format string specifically for `Debug` (highest priority)
244/// - `#[fmt("...", args...)]` - Shared format string for both `Display` and `Debug` (fallback)
245///
246/// # Example
247///
248/// ```
249/// use format_attr::DebugAttr;
250///
251/// #[derive(DebugAttr)]
252/// #[fmt_debug("User {{ name: {} }}", self.name)]
253/// struct User {
254///     name: String,
255/// }
256/// ```
257#[proc_macro_derive(DebugAttr, attributes(fmt, fmt_debug))]
258pub fn derive_debug(input: TokenStream) -> TokenStream {
259    let input = syn::parse_macro_input!(input as DeriveInput);
260
261    let name = &input.ident;
262    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
263
264    let fmt_args = match parse_debug_attr(&input.attrs) {
265        Some(args) => args,
266        None => {
267            return syn::Error::new_spanned(
268                &input,
269                "DebugAttr requires a #[fmt(...)] or #[fmt_debug(...)] attribute",
270            )
271            .to_compile_error()
272            .into();
273        }
274    };
275
276    let fmt_body = generate_fmt_body(&fmt_args);
277
278    let expanded = quote! {
279        impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
280            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281                #fmt_body
282            }
283        }
284    };
285
286    TokenStream::from(expanded)
287}
288
289/// Derive macro for implementing `std::fmt::Display` using the existing `Debug` implementation.
290///
291/// This macro requires that the type already implements `Debug`. It delegates the `Display`
292/// implementation to the `Debug` implementation, so both `{}` and `{:?}` will produce the same output.
293///
294/// # Example
295///
296/// ```
297/// use format_attr::DisplayAsDebug;
298///
299/// #[derive(Debug, DisplayAsDebug)]
300/// struct Value(i32);
301///
302/// let v = Value(42);
303/// assert_eq!(format!("{}", v), "Value(42)");
304/// assert_eq!(format!("{:?}", v), "Value(42)");
305/// ```
306#[proc_macro_derive(DisplayAsDebug)]
307pub fn derive_display_as_debug(input: TokenStream) -> TokenStream {
308    let input = syn::parse_macro_input!(input as DeriveInput);
309
310    let name = &input.ident;
311    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
312
313    let expanded = quote! {
314        impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
315            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316                std::fmt::Debug::fmt(self, f)
317            }
318        }
319    };
320
321    TokenStream::from(expanded)
322}