How does quote::format_ident! generate identifiers dynamically in procedural macros and why is it safer than string concatenation?
quote::format_ident! is a macro that creates syn::Ident values dynamically at compile time in procedural macros, combining format string syntax with identifier semantics. Unlike string concatenation which produces raw strings that require additional parsing to become identifiers, format_ident! produces proper Ident types that carry span information for error reporting and hygiene. The safety comes from type correctness (you get an Ident directly), proper span handling (errors point to the right location), and automatic validation (invalid identifier syntax is caught early rather than producing malformed output).
Basic format_ident! Usage
use quote::format_ident;
use syn::Ident;
fn generate_function_names() {
// Basic identifier generation
let func_name = format_ident!("my_function");
// Creates Ident("my_function", Span::call_site())
// With format arguments
let index = 0;
let name = format_ident!("func_{}", index);
// Creates Ident("func_0", Span::call_site())
// Multiple arguments
let prefix = "data";
let suffix = "processor";
let combined = format_ident!("{}_{}", prefix, suffix);
// Creates Ident("data_processor", Span::call_site())
}format_ident! produces Ident values with proper span information.
Using in Procedural Macros
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Generate builder name by adding "Builder" suffix
let builder_name = format_ident!("{}Builder", name);
// Generate method names for each field
let fields = match &input.data {
syn::Data::Struct(data) => {
match &data.fields {
syn::Fields::Named(fields) => &fields.named,
_ => panic!("Only named fields supported"),
}
}
_ => panic!("Only structs supported"),
};
let field_names: Vec<_> = fields.iter()
.map(|f| &f.ident)
.collect();
let builder_methods: Vec<_> = field_names.iter()
.map(|name| {
quote! {
pub fn #name(mut self, value: impl Into<String>) -> Self {
self.#name = Some(value.into());
self
}
}
})
.collect();
let expanded = quote! {
pub struct #builder_name {
#(#field_names: Option<String>),*
}
impl #builder_name {
#(#builder_methods)*
}
};
expanded.into()
}format_ident! generates builder names and preserves the span for error messages.
String Concatenation Approach
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident};
use proc_macro2::Span;
#[proc_macro_derive(BuilderBad)]
pub fn derive_builder_bad(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// WRONG: String concatenation approach
let builder_name_str = format!("{}Builder", name.to_string());
// Must manually parse string into Ident
// Loses span information from original identifier
let builder_name = Ident::new(&builder_name_str, Span::call_site());
// Problems:
// 1. Extra allocation for string
// 2. Lost span information (errors won't point to source)
// 3. Manual construction required
// 4. No compile-time validation
let expanded = quote! {
pub struct #builder_name {
// ...
}
};
expanded.into()
}String concatenation requires manual Ident construction and loses span information.
Span Handling for Error Messages
use quote::format_ident;
use syn::Ident;
use proc_macro2::Span;
fn demonstrate_spans() {
// format_ident! with explicit span
let span = Span::call_site();
let ident = format_ident!("my_ident", span = span);
// The ident carries span information
// When errors occur, they point to this location
// Using the ident in generated code
let tokens = quote! {
fn #ident() {
// Error messages will reference the span
}
};
// If this ident comes from a derive macro input,
// errors will point back to the original source location
}Span information connects generated code back to the source location.
Span Propagation from Input
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Getters)]
pub fn derive_getters(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// The span from 'name' carries over
let getter_prefix = format_ident!("get_{}", name);
// Uses name's span automatically
// Explicit span specification
let setter_name = format_ident!("set_{}", name.to_string().to_lowercase());
// Note: to_lowercase() creates new string, loses span
let better_setter = format_ident!("set_{}", name.to_string().to_lowercase());
// Still uses call_site span since we created a new string
quote! {
impl #name {
pub fn #getter_prefix(&self) -> &str {
"value"
}
}
}.into()
}Spans from input identifiers propagate to generated identifiers.
Safety Against Invalid Identifiers
use quote::format_ident;
fn demonstrate_validation() {
// Valid identifiers work fine
let valid = format_ident!("valid_identifier");
let also_valid = format_ident!("CamelCase");
let numeric_suffix = format_ident!("var_{}", 123); // var_123
// These would create invalid identifiers:
// format_ident!("123_starts_with_number"); // Compile error!
// format_ident!("has-hyphen"); // Compile error!
// format_ident!("has space"); // Compile error!
// format_ident!(""); // Empty identifier error!
// format_ident! validates at compile time
// String concatenation would create invalid code
// Raw identifiers for Rust keywords
let r#type = format_ident!("r#type"); // Escapes keyword
let r#fn = format_ident!("r#fn"); // Escapes keyword
}format_ident! catches invalid identifier patterns at compile time.
String Concatenation Dangers
use syn::Ident;
use proc_macro2::Span;
use quote::format_ident;
fn demonstrate_dangers() {
// DANGER: String concatenation can produce invalid identifiers
let user_input = "123bad"; // Starts with number
// String approach - no validation
let bad_ident_str = format!("var_{}", user_input);
let ident_from_str = Ident::new(&bad_ident_str, Span::call_site());
// Creates Ident("var_123bad") - but if user_input was just "123bad",
// the identifier would be invalid
// format_ident approach - validates
// let bad = format_ident!("{}", "123bad"); // Would fail validation
// Safe combination
let safe = format_ident!("var_{}", "123bad"); // var_123bad - valid!
// The prefix makes it valid
// DANGER: Special characters
let with_hyphen = "has-hyphen";
let dangerous = format!("func_{}", with_hyphen);
// func_has-hyphen - invalid identifier!
// But string concatenation won't tell you
// format_ident! would reject this
// format_ident!("func_{}", "has-hyphen"); // Error!
}String concatenation silently produces invalid identifiers; format_ident! validates.
Handling Rust Keywords
use quote::format_ident;
fn handle_keywords() {
// Rust keywords need special handling
// format_ident with 'r#' prefix for raw identifiers
let type_ident = format_ident!("r#type");
let fn_ident = format_ident!("r#fn");
// Can also use in code generation
let tokens = quote! {
fn #type_ident() -> i32 {
42
}
};
// For user input that might be keywords:
let field_name = "type"; // User-provided name
// Check if it's a keyword
let safe_name = if is_rust_keyword(field_name) {
format_ident!("r#{}", field_name)
} else {
format_ident!("{}", field_name)
};
}
fn is_rust_keyword(s: &str) -> bool {
matches!(s,
"as" | "break" | "const" | "continue" | "crate" | "else" | "enum" |
"extern" | "false" | "fn" | "for" | "if" | "impl" | "in" | "let" |
"loop" | "match" | "mod" | "move" | "mut" | "pub" | "ref" | "return" |
"self" | "Self" | "static" | "struct" | "super" | "trait" | "true" |
"type" | "unsafe" | "use" | "where" | "while" | "async" | "await" |
"dyn" | "abstract" | "become" | "box" | "do" | "final" | "macro" |
"override" | "priv" | "typeof" | "unsized" | "virtual" | "yield"
)
}format_ident! with r# prefix handles keywords correctly.
Generating Sequential Identifiers
use quote::{quote, format_ident};
fn generate_sequential() {
// Generate numbered identifiers
let vars: Vec<_> = (0..5)
.map(|i| format_ident!("var_{}", i))
.collect();
// var_0, var_1, var_2, var_3, var_4
let expanded = quote! {
#(
let #vars = #vars + 1;
)*
};
// Generate with padding
let padded: Vec<_> = (0..10)
.map(|i| format_ident!("item_{:02}", i))
.collect();
// item_00, item_01, ..., item_09
// Use in struct generation
let fields: Vec<_> = (0..3)
.map(|i| {
let field_name = format_ident!("field_{}", i);
quote! { #field_name: i32 }
})
.collect();
let struct_def = quote! {
struct MyStruct {
#(#fields),*
}
};
}Sequential identifiers are common in macro-generated code.
Case Conversions
use quote::format_ident;
use heck::SnakeCase;
fn case_conversions() {
let struct_name = "MyStruct";
// Snake case for fields
let field_name = format_ident!("{}", struct_name.to_snake_case());
// my_struct
// Manual case conversion
let lower = format_ident!("{}_lower", struct_name.to_lowercase());
// MyStruct_lower
let upper = format_ident!("{}_UPPER", struct_name.to_uppercase());
// MyStruct_UPPER
// Combining with indices
let indexed = format_ident!("{}_{}", struct_name, 0);
// MyStruct_0
// Be careful: case conversion creates new strings
// Span information is lost
let original = syn::Ident::new(struct_name, proc_macro2::Span::call_site());
let converted = format_ident!("{}", struct_name.to_lowercase());
// converted has call_site span, not original's span
}Case conversions create new strings, losing original span information.
Complete Derive Macro Example
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, DeriveInput, Data, Fields, Ident};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Original struct name
let name = &input.ident;
// Generate builder name using format_ident!
let builder_name = format_ident!("{}Builder", name);
// Extract fields
let fields = match &input.data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("Only named fields supported"),
}
}
_ => panic!("Only structs supported"),
};
// Generate field-related identifiers
let field_idents: Vec<&Ident> = fields.iter()
.map(|f| f.ident.as_ref().unwrap())
.collect();
let field_types: Vec<_> = fields.iter()
.map(|f| &f.ty)
.collect();
// Generate setter method names (same as field names)
let setter_names = field_idents.iter()
.map(|ident| format_ident!("set_{}", ident));
// Generate the builder implementation
let expanded = quote! {
pub struct #builder_name {
#(#field_idents: Option<#field_types>),*
}
impl #builder_name {
pub fn new() -> Self {
Self {
#(#field_idents: None),*
}
}
#(
pub fn #setter_names(mut self, value: #field_types) -> Self {
self.#field_idents = Some(value);
self
}
)*
pub fn build(self) -> Result<#name, &'static str> {
#(
if self.#field_idents.is_none() {
return Err("Missing field");
}
)*
Ok(#name {
#(#field_idents: self.#field_idents.unwrap()),*
})
}
}
impl #name {
pub fn builder() -> #builder_name {
#builder_name::new()
}
}
};
expanded.into()
}This macro uses format_ident! for clean identifier generation throughout.
Hygiene Considerations
use quote::{quote, format_ident};
use proc_macro2::Span;
fn hygiene_example() {
// Identifiers created by format_ident! in procedural macros
// are "unhygienic" - they work like regular identifiers
let local_var = format_ident!("local_var");
// This identifier can reference user code
let code = quote! {
fn example() {
let #local_var = 42;
println!("{}", #local_var);
}
};
// Hygiene matters more for macro_rules! macros
// In proc macros with quote!, identifiers are unhygienic by default
// Using Span::mixed_site() for intermediate hygiene
let mixed_span_ident = format_ident!("generated_ident", span = Span::mixed_site());
// This provides a middle ground for hygiene
}Procedural macro identifiers are generally unhygienic, interacting with user code.
Comparing Approaches
use syn::Ident;
use proc_macro2::Span;
use quote::format_ident;
fn comparison() {
// Approach 1: String concatenation (BAD)
let name = "MyStruct";
let builder_str = format!("{}Builder", name);
let builder_bad = Ident::new(&builder_str, Span::call_site());
// Problems:
// - Manual span handling
// - String allocation overhead
// - No validation
// - Verbose
// Approach 2: format_ident! (GOOD)
let builder_good = format_ident!("{}Builder", name);
// Benefits:
// - Automatic span handling
// - No intermediate string allocation in some cases
// - Validation at compile time
// - Concise
// Approach 3: Ident::new with explicit span (OK for specific cases)
let builder_explicit = Ident::new(&format!("{}Builder", name), Span::call_site());
// Use when you need explicit control over span
}format_ident! is cleaner, safer, and more idiomatic.
Edge Cases and Error Handling
use quote::format_ident;
use syn::Ident;
use proc_macro2::Span;
fn edge_cases() {
// Empty string - would be caught
// let empty = format_ident!(""); // Compile error!
// Starting with number - would be caught
// let num_start = format_ident!("123var"); // Compile error!
// But prefixing makes it valid
let prefixed = format_ident!("var_{}", 123); // var_123 - valid!
// Unicode identifiers (valid in Rust)
let unicode = format_ident!("ćé"); // Valid Chinese characters
let emoji_free = format_ident!("café"); // Valid with accents
// Escaping keywords
let r#match = format_ident!("r#match");
let r#box = format_ident!("r#box");
// Raw identifier without keyword is still valid
let regular = format_ident!("regular");
// Produces Ident("regular", span), not r#regular
}format_ident! handles edge cases with compile-time validation.
Performance Considerations
use quote::format_ident;
use syn::Ident;
use proc_macro2::Span;
fn performance() {
// format_ident! is evaluated at compile time
// No runtime cost in the generated code
// String concatenation also happens at compile time
// But requires more allocations
// For many identifiers in a loop:
let idents: Vec<_> = (0..1000)
.map(|i| format_ident!("var_{}", i))
.collect();
// This creates 1000 Ident values at compile time
// The format string is processed once
// String approach would be:
let strings: Vec<_> = (0..1000)
.map(|i| {
let s = format!("var_{}", i);
Ident::new(&s, Span::call_site())
})
.collect();
// Creates 1000 intermediate String allocations
}format_ident! avoids intermediate string allocations.
Synthesis
format_ident! characteristics:
- Creates
Identvalues directly - Preserves span information
- Validates identifier syntax
- Handles format arguments
- Works with Rust keywords via
r#prefix
String concatenation problems:
- Requires manual
Ident::newconstruction - Loses span information by default
- No validation until runtime
- More verbose
- Potential for invalid identifiers
When format_ident! is essential:
- Derive macros generating types
- Creating builder patterns
- Generating method names from fields
- Creating sequential identifiers
- Any procedural macro that creates new names
When string manipulation might suffice:
- Non-identifier string values
- Documentation generation
- Error messages
- Non-code output
Key insight: format_ident! isn't just a convenienceâit's a safety mechanism. It ensures that generated identifiers are syntactically valid, properly spanned for error messages, and correctly typed for use in generated code. String concatenation might work in simple cases but fails silently on edge cases and produces poor error messages. The macro handles the messy details of identifier creation so macro authors can focus on code generation logic. The span handling alone makes it worthwhileâerrors in generated code point back to meaningful locations rather than opaque call sites.
