How does quote::quote_spanned! preserve span information for better compiler error messages in proc macros?
quote::quote_spanned! assigns a specific source code span to the generated tokens, ensuring that compiler errors, warnings, and suggestions point to meaningful locations in the user's code rather than to opaque macro invocation sites. In procedural macros, code is generated programmatically, and by default the generated tokens have spans pointing to the macro definition site, which produces unhelpful error messages like "error inside procedural macro." quote_spanned! solves this by allowing macro authors to attach spans from input tokens to output tokens, so when the generated code has an error, the compiler can point to the exact location in the user's source that triggered the issue.
The Problem: Opaque Error Locations
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(MyTrait)]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
// Default quote! gives generated code a "call site" span
// Errors point to the macro invocation, not the actual source
let expanded = quote! {
impl MyTrait for #input {
fn method(&self) -> i32 {
// If user's type has an error, message points here
// instead of to the user's code
self.nonexistent_field // Error points to macro
}
}
};
expanded.into()
}Default quote! uses Span::call_site(), producing generic error locations.
What Are Spans?
use proc_macro2::Span;
// A Span represents a location in source code:
// - File path
// - Line number
// - Column start/end
// - Parent span (for macro expansion context)
// Spans are used for:
// - Error messages: "error at line 10, column 5"
// - IDE features: go to definition, hover info
// - Macro hygiene: variable capture rules
fn span_explanation() {
// Span::call_site(): Points to where macro was invoked
// Span::mixed_site(): Hybrid of call site and definition site
// Span::def_site(): Points to where macro is defined
// quote_spanned! allows using SPANS from input tokens
// so errors point to user's code, not macro internals
}Spans carry source location information that the compiler uses for diagnostics.
Basic quote_spanned! Usage
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Validate)]
pub fn derive_validate(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Get the span from the input type name
let type_name = &input.ident;
let type_span = type_name.span();
// Use quote_spanned! with that span
let expanded = quote_spanned! { type_span =>
impl Validate for #type_name {
fn validate(&self) -> Result<(), String> {
// Errors in this impl block will point to
// the user's type definition, not the macro
Ok(())
}
}
};
expanded.into()
}quote_spanned! assigns the specified span to all generated tokens.
Error Message Comparison
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput, Data, Field};
#[proc_macro_derive(DebugCustom)]
pub fn derive_debug_custom(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// WITHOUT quote_spanned - poor error location
let poor = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// Error: "missing field `unknown_field` in MyStruct"
// Points to macro invocation, not to struct definition
write!(f, "MyStruct {{ unknown_field: {:?} }}", self.unknown_field)
}
}
};
// WITH quote_spanned - error points to user's code
let good = quote_spanned! { name.span() =>
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// Error points to `name` in user's source
write!(f, "{} {{ ... }}", stringify!(#name))
}
}
};
good.into()
}With quote_spanned!, errors point to the specific token that provided the span.
Attaching Spans from Field Names
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;
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 setter methods with proper spans
let setters = fields.iter().map(|field| {
let field_name = &field.ident;
let field_ty = &field.ty;
let span = field_name.as_ref().unwrap().span();
// Each setter gets the span of its field
quote_spanned! { span =>
fn #field_name(&mut self, value: #field_ty) -> &mut Self {
self.#field_name = Some(value);
self
}
}
});
let expanded = quote_spanned! { name.span() =>
impl Builder for #name {
#(#setters)*
}
};
expanded.into()
}Each field's setter method gets that field's span for targeted error messages.
Error Pointing to Field Definition
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Serialize)]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
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"),
};
let serialize_fields = fields.iter().map(|field| {
let field_name = &field.ident;
let field_span = field_name.as_ref().unwrap().span();
// Error in field serialization points to field definition
quote_spanned! { field_span =>
// If field doesn't implement Serialize,
// error points to field definition in struct
#field_name: self.#field_name.serialize(),
}
});
let expanded = quote_spanned! { name.span() =>
impl Serialize for #name {
fn serialize(&self) -> String {
format!("{{ {} }}", [#(#serialize_fields),*].join(", "))
}
}
};
expanded.into()
}
// User's code:
// struct User {
// name: String,
// age: NonSerializable, // Error points HERE
// }
//
// #[derive(Serialize)]
// struct User { ... }Errors about field types point to the field definition in the struct.
Span Propagation Chain
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Ident, Type};
#[proc_macro_attribute]
pub fn validate(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
// Span propagation chain:
// 1. User writes `struct Foo { x: u32 }`
// 2. `name` gets span of `Foo`
// 3. Generated impl uses `name.span()`
// 4. Errors point to `Foo` in user's code
let name_span = name.span();
let expanded = quote_spanned! { name_span =>
impl Validate for #name {
fn validate(&self) -> bool {
true
}
}
};
expanded.into()
}
// Spans flow from input tokens through to output tokens:
// - Input: `struct MyStruct { ... }`
// - `MyStruct` token has a Span (file, line, column)
// - quote_spanned! attaches that Span to generated code
// - Errors reference that Span, pointing to `MyStruct`Spans flow from input tokens to generated code, maintaining source location.
Different Span Sources
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Field};
#[proc_macro_derive(Validator)]
pub fn derive_validator(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("Only named fields"),
},
_ => panic!("Only structs"),
};
// Different span sources for different purposes:
// 1. Type name span - for impl block errors
let type_span = name.span();
// 2. Field name span - for field-specific errors
let field_spans: Vec<_> = fields.iter()
.map(|f| f.ident.as_ref().unwrap().span())
.collect();
// 3. Field type span - for type mismatch errors
let type_spans: Vec<_> = fields.iter()
.map(|f| f.ty.span())
.collect();
// Use appropriate span for each error context
let validations = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
let field_span = field_name.span();
// Field-specific validation with field span
quote_spanned! { field_span =>
if !self.#field_name.validate() {
// Error points to this field in struct
return false;
}
}
});
let expanded = quote_spanned! { type_span =>
impl Validator for #name {
fn validate(&self) -> bool {
#(#validations)*
true
}
}
};
expanded.into()
}Different spans are appropriate for different error contexts.
Span for Generated Code vs Input Code
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Clone)]
pub fn derive_clone(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// For the impl block, use type span
// This way, impl errors point to the type definition
let name_span = name.span();
// Regular quote! for internal implementation details
// where we don't want to point to user's code
let internal_code = quote! {
// These spans point to macro definition
// Acceptable for internal implementation
fn clone(&self) -> Self {
Self { /* ... */ }
}
};
// quote_spanned! for user-facing parts
let expanded = quote_spanned! { name_span =>
impl Clone for #name {
#internal_code
}
};
expanded.into()
}Use quote_spanned! for user-facing code, quote! for internal details.
Multiple Span Sources
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Accessors)]
pub fn derive_accessors(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("Only named fields"),
},
_ => panic!("Only structs"),
};
let accessors = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
// Span for getter: points to field name
let getter_span = field_name.span();
// Span for return type: points to field type
let type_span = field_ty.span();
// Getter method
let getter = quote_spanned! { getter_span =>
pub fn #field_name(&self) -> &#field_ty {
&self.#field_name
}
};
// Setter method
let setter = quote_spanned! { getter_span =>
pub fn set_#field_name(&mut self, value: #field_ty) {
self.#field_name = value;
}
};
quote! {
#getter
#setter
}
});
let impl_span = name.span();
let expanded = quote_spanned! { impl_span =>
impl #name {
#(#accessors)*
}
};
expanded.into()
}Each generated method can use the span of its corresponding input element.
Hygiene and Macros
use proc_macro::TokenStream;
use quote::quote_spanned;
use proc_macro2::Span;
// Spans affect macro hygiene - which variables can be referenced
#[proc_macro]
pub fn create_variable(input: TokenStream) -> TokenStream {
// Without proper spans, generated variables might:
// - Shadow user variables unexpectedly
// - Be inaccessible from user code
// - Have confusing error messages
let var_name = syn::Ident::new("internal_var", Span::call_site());
let user_span = Span::call_site(); // Would use actual input span
// Using call_site span means:
// - Variable visible in same scope as macro call
// - Can shadow user variables
// - Errors point to macro call location
let expanded = quote_spanned! { user_span =>
let #var_name = 42;
};
expanded.into()
}
// Better: Use def_site span for internal variables
// This makes them "hygienic" - won't conflict with user codeSpans affect variable hygiene and scoping in macro-generated code.
Working with Error Types
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, parse::Error};
#[proc_macro_derive(TryFrom)]
pub fn derive_try_from(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let name_span = name.span();
// Compile-time errors with proper spans
let expanded = match validate_derive(&input) {
Ok(_) => {
quote_spanned! { name_span =>
impl std::convert::TryFrom<i32> for #name {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
Ok(Self {})
}
}
}
}
Err(err_msg) => {
// Error points to the type that failed validation
quote_spanned! { name_span =>
compile_error!(#err_msg);
}
}
};
expanded.into()
}
fn validate_derive(input: &DeriveInput) -> Result<(), &'static str> {
// Validation logic
Ok(())
}Use quote_spanned! with compile_error! for targeted compile-time diagnostics.
Practical Example: Builder Pattern
use proc_macro::TokenStream;
use quote::quote_spanned;
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);
let name = &input.ident;
let builder_name = Ident::new(&format!("{}Builder", name), name.span());
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("Only named fields"),
},
_ => panic!("Only structs"),
};
// Each field's optional type in builder
let builder_fields = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
let span = field_name.span();
// Error if field type doesn't support Option
// will point to this field definition
quote_spanned! { span =>
#field_name: Option<#field_ty>
}
});
// Build method
let build_impl = quote_spanned! { name.span() =>
pub fn build(self) -> Result<#name, &'static str> {
// If fields missing, error points to struct
Ok(#name {
// Field initialization...
})
}
};
let expanded = quote_spanned! { name.span() =>
pub struct #builder_name {
#(#builder_fields),*
}
impl #builder_name {
#build_impl
}
};
expanded.into()
}The builder struct and its methods get spans from the original struct definition.
Debugging Macro Spans
use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(DebugSpan)]
pub fn derive_debug_span(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Span information for debugging
let span = name.span();
// Span provides:
// - start line, column
// - end line, column
// - source file (in nightly/experimental)
// Note: In stable Rust, you can't directly access
// file/line info from Span, but the compiler uses it
// for error messages
// The span affects where errors point:
// - call_site: macro invocation
// - def_site: macro definition
// - mixed_site: hybrid
// - Custom span from input token: input location
let expanded = quote_spanned! { span =>
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", stringify!(#name))
}
}
};
expanded.into()
}Spans carry location information used by the compiler for diagnostics.
When to Use quote_spanned! vs quote!
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Example)]
pub fn derive_example(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let name_span = name.span();
// Use quote_spanned! for:
// - impl blocks (point to type definition)
// - Trait method errors (point to trait requirements)
// - Field access errors (point to field definition)
// - Type constraint errors (point to where clause)
let impl_block = quote_spanned! { name_span =>
impl Trait for #name {
fn method(&self) -> i32 {
42
}
}
};
// Use quote! for:
// - Internal helper functions
// - Macro implementation details
// - Code that shouldn't surface errors to user
// - Generated identifiers the user didn't write
let helper = quote! {
fn internal_helper() -> i32 {
// Errors here point to macro, acceptable
42
}
};
let expanded = quote! {
#impl_block
#helper
};
expanded.into()
}Use quote_spanned! for user-facing code, quote! for internal details.
Synthesis
Span basics:
// Spans carry source location:
// - File, line, column information
// - Used for error messages and IDE features
// - Default: Span::call_site() points to macro invocation
// - Custom spans from input tokens point to user's code
// quote! uses call_site span
let code = quote! { /* error points to macro call */ };
// quote_spanned! uses provided span
let code = quote_spanned! { user_span => /* error points to user's code */ };When to use each:
// Use quote_spanned! when:
// - Generating impl blocks
// - Accessing user-defined fields
// - Creating methods for user types
// - Error messages should point to user's code
// Use quote! when:
// - Internal implementation details
// - Generated helper code
// - Code without user-facing errors
// - Performance-critical sections (slightly faster)Error message impact:
// Without quote_spanned!:
// error[E0308]: mismatched types
// --> src/main.rs:5:10
// |
// 5 | #[derive(MyMacro)]
// | ^^^^^^^ expected `i32`, found `String`
// |
// (Points to macro invocation, unhelpful)
// With quote_spanned!:
// error[E0308]: mismatched types
// --> src/main.rs:7:5
// |
// 7 | field: String, // expected `i32`
// | ^^^^^^^^^^^^^ expected `i32`, found `String`
// |
// (Points to actual field, helpful)Key insight: quote::quote_spanned! bridges the gap between generated code and user source code by attaching meaningful spans to generated tokens. Without explicit span management, all generated code has spans pointing to the macro invocation site, producing error messages that say "error in macro invocation" without context. With quote_spanned!, macro authors can propagate spans from input tokens to output tokens, so when the compiler encounters an error in generated code, it can point to the specific location in the user's source that caused the problem—a field that has the wrong type, a trait bound that isn't satisfied, or a struct that doesn't meet macro requirements. This transforms proc macros from opaque code generators into well-integrated language extensions that participate properly in the compiler's diagnostic system.
