How does quote::quote_spanned! differ from quote! for preserving span information in procedural macros?
quote! generates token streams with synthetic spans that point to the macro invocation site, while quote_spanned! lets you explicitly control span information so error messages, warnings, and IDE features point to specific locations in the user's source code. In procedural macros, spans determine where compiler diagnostics appear and how identifiers resolve across hygiene boundaries. quote_spanned! is essential when you need errors to point to specific tokens from the input rather than the macro definition, or when generating code where specific parts should be attributed to specific source locations for proper error reporting.
Basic quote! Usage
use proc_macro2::TokenStream;
use quote::quote;
fn generate_getter(field_name: &syn::Ident) -> TokenStream {
// quote! generates tokens with call-site spans
// Error messages will point to where this macro is invoked
quote! {
pub fn #field_name(&self) -> &str {
&self.#field_name
}
}
}
// If the generated code has an error, it points to:
// - The macro invocation site (not helpful for user)
// - Not the field definition in user's codequote! uses synthetic spans based on where the macro is invoked.
The Span Problem in Procedural Macros
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// With plain quote!, errors point here, not at user's code
let name = &input.ident;
let expanded = quote! {
impl #name {
fn validate(&self) -> Result<(), &'static str> {
// If this code has an error, user sees error at macro site
// Not at their struct definition
Err("not implemented")
}
}
};
expanded.into()
}Errors from generated code point to macro invocation, not relevant user code.
quote_spanned! for Precise Error Locations
use proc_macro2::Span;
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Field};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Get span from the struct definition
let struct_span = name.span();
// Use quote_spanned! to set error location
let check_code = if let Data::Struct(data) = &input.data {
match &data.fields {
Fields::Named(fields) => {
fields.named.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_span = field_name.span();
// Error for this field points to the field definition
quote_spanned! {field_span=>
if self.#field_name.is_none() {
return Err(concat!("Missing field: ", stringify!(#field_name)));
}
}
}).collect::<proc_macro2::TokenStream>()
}
_ => quote! {}
}
} else {
quote! {}
};
let expanded = quote! {
impl #name {
fn validate(&self) -> Result<(), &'static str> {
#check_code
Ok(())
}
}
};
expanded.into()
}quote_spanned! makes errors point to specific fields in user's code.
How Spans Affect Error Messages
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
fn demonstrate_span_difference() -> TokenStream {
// Imagine this is in a derive macro
// Using quote!: error points to macro invocation
let code1 = quote! {
nonexistent_function(); // Error points to macro call site
};
// Using quote_spanned!: error points to specific span
// In real code, this span comes from input tokens
use proc_macro2::Span;
let specific_span = Span::call_site(); // In practice, use input.span()
let code2 = quote_spanned! {specific_span=>
nonexistent_function(); // Error points to specific_span location
};
code1
}Spans determine where error messages appear in the user's source.
Span Propagation from Input Tokens
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Validate)]
pub fn derive_validate(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let validations = if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
fields.named.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
// Use the field's span for error messages
// This makes errors point to the field in user's code
let field_span = field_name.span();
quote_spanned! {field_span=>
// Errors in this block point to field_name's location
#field_name: <#field_ty>::validate(&self.#field_name)?;
}
}).collect::<proc_macro2::TokenStream>()
} else {
quote! {}
}
} else {
quote! {}
};
let expanded = quote! {
impl Validate for #name {
fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
#validations
Ok(())
}
}
};
expanded.into()
}
trait Validate {
fn validate(&self) -> Result<(), Box<dyn std::error::Error>>;
}Each field's validation error points to that field's definition.
Error Pointing to Struct Definition
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Ident};
#[proc_macro_derive(Constructor)]
pub fn derive_constructor(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Span from the struct's name
let struct_span = input.ident.span();
// Generate compile_error! pointing to the struct
let error_code = quote_spanned! {struct_span=>
compile_error!("This struct cannot derive Constructor");
};
// When this error fires, it points to the struct name
error_code.into()
}
// User's code:
// #[derive(Constructor)]
// struct MyStruct { ... }
// Error: "This struct cannot derive Constructor" points to "MyStruct"compile_error! with proper span shows errors at meaningful locations.
Spanned Method Generation
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Field, Type, Ident};
#[proc_macro_derive(Getters)]
pub fn derive_getters(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let getters = if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
fields.named.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
// Method name uses field's span
// This affects IDE support and error messages
let method_span = field_name.span();
quote_spanned! {method_span=>
pub fn #field_name(&self) -> &#field_ty {
&self.#field_name
}
}
}).collect::<proc_macro2::TokenStream>()
} else {
quote! {}
}
} else {
quote! {}
};
let expanded = quote! {
impl #struct_name {
#getters
}
};
expanded.into()
}Getter methods inherit spans from their corresponding fields.
Comparison: quote! vs quote_spanned!
use proc_macro2::Span;
use quote::{quote, quote_spanned};
fn compare_approaches(input_ident: syn::Ident) -> proc_macro2::TokenStream {
// quote! - all spans point to call site
let with_quote = quote! {
let val = #input_ident; // Error here points to macro call site
};
// quote_spanned! - spans controlled explicitly
let input_span = input_ident.span();
let with_spanned = quote_spanned! {input_span=>
let val = #input_ident; // Error here points to input_ident's definition
};
// Practical difference:
// - quote!: "error at line 5 (where macro was invoked)"
// - quote_spanned!: "error at line 12 (where the identifier was defined)"
with_quote
}The difference is where error messages point in the source.
Multi-Token Spanning
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Span the entire generated method with the struct's span
let struct_span = name.span();
let builder_methods = if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
fields.named.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
// Each field gets its own span
let field_span = field_name.span();
quote_spanned! {field_span=>
pub fn #field_name(mut self, value: #field_ty) -> Self {
self.#field_name = Some(value);
self
}
}
}).collect::<proc_macro2::TokenStream>()
} else {
quote! {}
}
} else {
quote! {}
};
// The impl block itself can have a different span
let expanded = quote_spanned! {struct_span=>
impl Builder for #name {
type Builder = #name Builder;
fn builder() -> Self::Builder {
#name Builder::default()
}
}
};
expanded.into()
}
trait Builder {
type Builder;
fn builder() -> Self::Builder;
}Different parts of generated code can have different spans.
Hygiene and Spans
use proc_macro2::Span;
use quote::{quote, quote_spanned};
// Spans affect identifier hygiene
fn hygiene_example() {
// quote! creates identifiers at call_site
// These are "unhygienic" - they can shadow user variables
let code1 = quote! {
let internal_var = 42; // May shadow user's internal_var
};
// quote_spanned! with mixed_site creates hygienic identifiers
// These won't interfere with user variables
let span = Span::mixed_site();
let code2 = quote_spanned! {span=>
let internal_var = 42; // Won't shadow user variables
};
// Span::call_site() - visible to user, can shadow
// Span::mixed_site() - hygienic, won't shadow user vars
// Span::def_site() - defined at macro definition site
}Spans control hygieneāwhether generated identifiers can shadow user identifiers.
Practical Pattern: Proper Error Attribution
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Error};
#[proc_macro_derive(ValidateFields)]
pub fn derive_validate_fields(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Generate compile-time validation
let validations = if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
fields.named.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
let field_span = field_name.span();
// Check if field type implements Validate
// Error points to field if validation fails
quote_spanned! {field_span=>
<#field_ty as Validate>::validate(&self.#field_name)
.map_err(|e| {
// Error context includes field name
e.context(stringify!(#field_name))
})?;
}
}).collect::<proc_macro2::TokenStream>()
} else {
quote! {}
}
} else {
// Emit compile error pointing to struct
let err = Error::new_spanned(&input, "Only structs with named fields supported");
return err.to_compile_error().into();
};
let expanded = quote! {
impl ValidateFields for #name {
fn validate_fields(&self) -> Result<(), ValidationError> {
#validations
Ok(())
}
}
};
expanded.into()
}
trait Validate {
fn validate(&self) -> Result<(), ValidationError>;
fn context(self, ctx: &'static str) -> ValidationError;
}
struct ValidationError;
impl ValidationError {
fn context(self, _ctx: &'static str) -> Self { self }
}Errors from generated code point to the fields they relate to.
Debugging with Spans
use proc_macro2::Span;
use quote::quote_spanned;
use syn::Ident;
fn debug_spans() {
// When developing procedural macros, understanding spans helps debugging
// call_site() - Where the macro is invoked
let call_span = Span::call_site();
let code1 = quote_spanned! {call_span=>
println!("This error points to macro invocation");
};
// def_site() - Where the macro is defined (less useful)
let def_span = Span::def_site();
let code2 = quote_spanned! {def_span=>
println!("This error points to macro definition");
};
// mixed_site() - Hybrid hygiene
let mixed_span = Span::mixed_site();
let code3 = quote_spanned! {mixed_span=>
println!("Hygienic identifiers");
};
// In practice: use input spans for user-attributed errors
// input_ident.span() - Points to where identifier appears in user code
}Different span types serve different purposes.
Real-World Example: Error Attribution in derive Macros
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Type, Error};
#[proc_macro_derive(Serialize)]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Validate all fields implement Serialize
let fields = match &input.data {
Data::Struct(data) => &data.fields,
_ => {
let err = Error::new_spanned(&input, "Only structs supported");
return err.to_compile_error().into();
}
};
let field_serializers = fields.iter().map(|field| {
let field_name = field.ident.as_ref();
let field_ty = &field.ty;
// Get the span of the field type for error attribution
let ty_span = field_ty.span();
// If field type doesn't implement Serialize,
// error points to the type, not the macro
match field_name {
Some(name) => {
let field_span = name.span();
quote_spanned! {field_span=>
#name: <#field_ty as Serialize>::serialize(&self.#name)?,
}
}
None => {
// Tuple struct field - use type span
quote_spanned! {ty_span=>
<#field_ty as Serialize>::serialize(&self.0)?,
}
}
}
});
let expanded = quote! {
impl Serialize for #name {
fn serialize(&self) -> Result<String, SerializeError> {
// ...
Ok(String::new())
}
}
};
expanded.into()
}
trait Serialize {
fn serialize(&self) -> Result<String, SerializeError>;
}
struct SerializeError;Type errors in generated code point to the field types in user's struct.
When to Use Each
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
fn when_to_use_which() {
// Use quote! when:
// - Generated code errors should point to macro call site
// - You're generating helper code internal to the macro
// - Spans don't matter for the generated code
let internal_code = quote! {
fn internal_helper() { /* ... */ }
};
// Use quote_spanned! when:
// - Errors should point to specific user code locations
// - You're generating code based on user input
// - IDE support should show proper hover/goto
// - Hygiene matters for generated identifiers
// In derive macros:
// - Use input token spans for validation errors
// - Use field spans for field-specific generated code
// - Use struct span for struct-wide generated code
}Choose based on where you want errors to point.
Synthesis
Quick reference:
use quote::{quote, quote_spanned};
use proc_macro2::Span;
// quote!: All spans point to call site (macro invocation)
let code1 = quote! {
fn generated() { /* error points to macro call site */ }
};
// quote_spanned!: Explicit span control
let user_span = some_input_token.span();
let code2 = quote_spanned! {user_span=>
fn generated() { /* error points to user_span location */ }
};
// Common span sources:
// - input.ident.span() - struct/enum name
// - field.ident.span() - field name
// - field.ty.span() - field type
// - Span::call_site() - macro invocation location
// - Span::mixed_site() - hygienic identifiers
// Use quote_spanned! for:
// - Derive macros where errors should point to fields
// - Attribute macros with input token attribution
// - Generated code that should integrate with user's IDEKey insight: The difference between quote! and quote_spanned! is fundamentally about error attribution and IDE support. quote! generates token streams where all locations resolve to the macro call siteāerrors appear at the #[derive(...)] attribute. quote_spanned! lets you preserve span information from input tokens so that errors, warnings, and IDE features point to specific locations in the user's source code. In derive macros, this means using each input token's span for generated code related to that token: field spans for field-specific methods, type spans for type errors, struct spans for struct-wide code. This dramatically improves the developer experience because diagnostics appear where they're meaningful rather than at an opaque macro invocation.
