What is the purpose of quote::quote_spanned for preserving span information in procedural macro output?
quote_spanned generates TokenStream with specific span information attached to all tokens, ensuring that compiler errors, warnings, and IDE features point to the correct location in the user's source code rather than to the macro definition. When writing procedural macros, preserving span information is critical for good developer experience: errors should point to where the problem is in the user's code, not somewhere inside the macro implementation. Without proper span handling, error messages become confusing and IDE features like go-to-definition break.
Understanding Spans in Procedural Macros
use proc_macro2::Span;
use quote::quote;
use syn::{Ident, parse_quote};
fn spans_explained() {
// A Span represents a location in source code
// It contains: file location, line, column, and context
// When the compiler reports errors, it uses spans to show where
// Without correct spans, errors point to wrong locations
let user_input: Ident = parse_quote!( my_identifier );
// This identifier has a span pointing to the user's source
// Using quote! creates new spans (usually call_site)
let output = quote! {
let #user_input = 42; // user_input keeps its original span
};
// The `let`, `=`, `42`, and `;` have synthetic spans
// Using quote_spanned! attaches a specific span to ALL generated tokens
let span = user_input.span();
let output = quote_spanned! { span =>
let x = 42; // All tokens here have span pointing to user_input's location
};
}Spans track source locations for error reporting and IDE support.
The Problem with Default quote! Behavior
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident};
// Example: A derive macro that generates incorrect code
#[proc_macro_derive(MyTrait)]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Problem: Using quote! loses span information
let output = quote! {
impl MyTrait for #input {
fn method(&self) -> Result<(), String> {
// This error points to the wrong location!
// The compiler will show this line in the macro,
// not in the user's code
Err("something went wrong".to_string())
}
}
};
output.into()
}
// If user writes:
// #[derive(MyTrait)]
// struct MyStruct;
//
// And the generated code has an error, the error message will
// point to the macro definition location, not the user's structDefault quote! uses Span::call_site() which points to the macro invocation.
Using quote_spanned! to Preserve Spans
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput, Data, Fields, Ident};
#[proc_macro_derive(Validate)]
pub fn derive_validate(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let span = name.span(); // Get the span from user's identifier
// Generate validation with proper error location
let validation_code = match &input.data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(fields) => {
let checks: Vec<_> = fields.named.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_span = field_name.span();
// Use quote_spanned! to attach field's span to checks
// This way, errors point to the field, not the macro
quote_spanned! { field_span =>
if self.#field_name.is_empty() {
return Err(concat!("Field ", stringify!(#field_name), " cannot be empty"));
}
}
}).collect();
quote! {
#(#checks)*
Ok(())
}
}
_ => quote! { Ok(()) }
}
}
_ => quote! { Ok(()) }
};
let output = quote! {
impl Validate for #name {
fn validate(&self) -> Result<(), String> {
#validation_code
}
}
};
output.into()
}quote_spanned! ensures errors reference the correct source location.
Error Message Quality Comparison
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput, Ident};
// WITHOUT quote_spanned: Poor error messages
#[proc_macro_derive(BadTrait)]
pub fn derive_bad_trait(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let output = quote! {
impl BadTrait for #name {
fn required_method(&self) -> i32 {
// Error here points to macro source file
"wrong type" // Type mismatch error shows macro location
}
}
};
output.into()
}
// WITH quote_spanned: Good error messages
#[proc_macro_derive(GoodTrait)]
pub fn derive_good_trait(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let span = name.span();
// quote_spanned! attaches the name's span to generated code
let output = quote_spanned! { span =>
impl GoodTrait for #name {
fn required_method(&self) -> i32 {
// Error here points to user's struct definition
"wrong type"
}
}
};
output.into()
}
// User code:
// #[derive(BadTrait)]
// struct MyStruct; // Error points here with GoodTrait
//
// #[derive(GoodTrait)]
// struct AnotherStruct; // Error correctly points hereThe span determines where the compiler shows errors.
Span Origins in Procedural Macros
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{Ident, Expr, parse_quote};
fn span_origins() {
// Different span sources:
// 1. call_site() - Where the macro was invoked
let call_site_span = Span::call_site();
// Used by default in quote!
// 2. mixed_site() - Combines call_site and def_site contexts
let mixed_span = Span::mixed_site();
// Useful for hygiene in certain contexts
// 3. def_site() - Where the macro is defined (unstable)
// let def_span = Span::def_site(); // Requires unstable feature
// 4. From user input - Best for error messages
let user_ident: Ident = parse_quote!(user_input);
let user_span = user_ident.span();
// This points to where user wrote "user_input"
// Practical example:
let from_user: Expr = parse_quote!(x + y);
// This entire expression has a span pointing to user code
// We can extract specific spans:
// If from_user came from parsing user input, its span points to that input
let user_span = from_user.span();
// Generate code with that span:
let output = quote_spanned! { user_span =>
let result = #from_user;
};
}Spans come from different sources; user input spans provide the best error locations.
Real-World Example: Derive Macro with Good Error Messages
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{
parse_macro_input, DeriveInput, Data, Fields, Field, Ident, Type,
parse_quote, Result, Error
};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match derive_builder_impl(input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn derive_builder_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
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,
_ => {
// Error with correct span pointing to struct
return Err(Error::new_spanned(
&input,
"Builder derive only supports structs with named fields"
));
}
}
}
_ => {
return Err(Error::new_spanned(
&input,
"Builder derive only supports structs"
));
}
};
let field_names: Vec<_> = fields.iter()
.map(|f| f.ident.as_ref().unwrap())
.collect();
let field_types: Vec<_> = fields.iter()
.map(|f| &f.ty)
.collect();
// Generate builder fields with correct spans
let builder_fields: Vec<_> = fields.iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
let ty = &field.ty;
let span = field.span();
// Each field's option uses that field's span
quote_spanned! { span =>
#name: Option<#ty>
}
})
.collect();
// Generate setter methods with field spans
let setters: Vec<_> = fields.iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
let ty = &field.ty;
let span = field.span();
// Setter validates with correct span
quote_spanned! { span =>
pub fn #name(&mut self, value: #ty) -> &mut Self {
self.#name = Some(value);
self
}
}
})
.collect();
// Build method that checks required fields
let build_checks: Vec<_> = fields.iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
let span = field.span();
// Error points to the missing field
quote_spanned! { span =>
if self.#name.is_none() {
return Err(concat!("Missing field: ", stringify!(#name)));
}
}
})
.collect();
let field_assigns: Vec<_> = field_names.iter()
.map(|name| {
quote! { #name: self.#name.unwrap() }
})
.collect();
let output = quote! {
pub struct #builder_name {
#(#builder_fields),*
}
impl #builder_name {
#(#setters)*
pub fn build(&self) -> Result<#name, &'static str> {
#(#build_checks)*
Ok(#name {
#(#field_assigns),*
})
}
}
impl #name {
pub fn builder() -> #builder_name {
#builder_name {
#(#field_names: None),*
}
}
}
};
Ok(output)
}This macro generates errors that point to the specific field causing problems.
Hygiene and Spans
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{Ident, parse_quote};
fn hygiene_example() {
// Hygiene determines whether identifiers from different scopes can conflict
let user_var: Ident = parse_quote!(x);
let user_span = user_var.span();
// Using quote! - identifiers are hygienic
let code1 = quote! {
let x = 1; // This 'x' is different from user's 'x'
let result = #user_var + x; // Uses both
};
// Using quote_spanned! - spans affect hygiene
let code2 = quote_spanned! { user_span =>
let x = 1; // This 'x' might conflict depending on span
let result = #user_var + x;
};
// Practical hygiene concern: don't let macro internals conflict with user code
let internal_prefix = "_macro_internal_";
let internal_var = Ident::new(
&format!("{}temp", internal_prefix),
Span::mixed_site() // Use mixed_site for internal variables
);
let output = quote! {
let #internal_var = 42; // Unlikely to conflict with user code
};
}Spans affect identifier hygiene, determining whether names conflict.
IDE Support and Spans
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput, Ident};
#[proc_macro_attribute]
pub fn cached(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
let span = name.span();
// WITHOUT quote_spanned:
// - Go-to-definition might not work correctly
// - IDE hover shows generic info
// - Rename refactoring might miss generated code
// WITH quote_spanned:
// - IDE features work better
// - Errors and warnings show correct locations
// - Documentation links work properly
let output = quote_spanned! { span =>
// Generated code with proper spans for IDE support
#input
impl Cached for #name {
fn cache_key(&self) -> String {
// Error messages from here will point to user's struct
format!("{:?}", self)
}
}
};
output.into()
}Proper spans enable IDE features like go-to-definition and rename refactoring.
Common Patterns for Span Usage
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{
parse_macro_input, DeriveInput, Data, Fields, Field, Ident, Type,
TypePath, PathArguments, AngleBracketedGenericArguments, GenericArgument,
parse_quote, Result, Error
};
// Pattern 1: Use input span for top-level generated items
fn pattern_input_span(input: &DeriveInput) -> proc_macro2::TokenStream {
let name = &input.ident;
let span = input.span(); // Use entire input's span
quote_spanned! { span =>
impl SomeTrait for #name {}
}
}
// Pattern 2: Use field span for field-specific errors
fn pattern_field_span(fields: &Fields) -> Vec<proc_macro2::TokenStream> {
fields.iter().map(|field| {
let span = field.span();
let name = field.ident.as_ref().unwrap();
quote_spanned! { span =>
// Errors/warnings here point to the specific field
println!("Field: {}", stringify!(#name));
}
}).collect()
}
// Pattern 3: Use type span for type-related validation
fn pattern_type_span(field: &Field) -> Result<proc_macro2::TokenStream> {
let field_name = field.ident.as_ref().unwrap();
let ty = &field.ty;
let span = ty.span(); // Span pointing to the type
// Validate the type and emit errors at type location
if let Type::Path(TypePath { path, .. }) = ty {
if let Some(segment) = path.segments.last() {
if segment.ident == "String" {
return Ok(quote_spanned! { span =>
// Code with span pointing to the String type
#field_name: String
});
}
}
}
// Emit error at type's location
Err(Error::new(span, "Expected String type"))
}
// Pattern 4: Combine spans for multi-location context
fn pattern_combined_span(input: &DeriveInput, field: &Field) -> proc_macro2::TokenStream {
let struct_name = &input.ident;
let field_name = field.ident.as_ref().unwrap();
let field_span = field.span();
// Use field span for the validation, but reference struct in error message
quote_spanned! { field_span =>
// Validation code that will point to field on error
if self.#field_name.is_none() {
return Err(stringify!(#struct_name must have #field_name));
}
}
}Choose spans based on what should be highlighted when errors occur.
Debugging Span Issues
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{Ident, parse_quote};
fn debugging_spans() {
// Enable span debugging with RUSTFLAGS
// RUSTFLAGS="-Z macro-backtrace" cargo build
// Check span locations programmatically (for debugging)
let ident: Ident = parse_quote!(example);
let span = ident.span();
// In nightly, you can inspect span details:
// span.start() - line/column where span starts
// span.end() - line/column where span ends
// span.source_file() - the source file
// For debugging generated code:
let code = quote_spanned! { span =>
fn example() {
let x = 42;
}
};
// Print the generated code to see what was produced
println!("Generated: {}", code);
// Use compile_error! to force errors at specific spans during development
let debug_code = quote_spanned! { span =>
compile_error!("Debug: check this span location");
};
}
// Helper macro for development
macro_rules! span_debug {
($span:expr, $msg:expr) => {
quote_spanned! { $span =>
compile_error!($msg)
}
};
}Debug spans using compiler flags and explicit error insertion.
Practical Guidelines
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput, Ident};
#[proc_macro_derive(Example)]
pub fn derive_example(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Guidelines:
// 1. Use quote_spanned! for code that might produce errors
// Span should point to where the user should look
// 2. Use quote! for:
// - Internal helper functions
// - Standard library usage
// - Code that cannot error in user-visible ways
// 3. Get spans from the most specific user input
// - Field spans for field-specific code
// - Type spans for type-related errors
// - Struct/enum spans for whole-item code
let name = &input.ident;
let span = name.span();
// Top-level impl: use struct name's span
let impl_block = quote_spanned! { span =>
impl Example for #name {
fn example(&self) -> String {
// This method's span points to struct
String::from("example")
}
}
};
impl_block.into()
}Choose spans that guide users to the right location for fixes.
Synthesis
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput};
// Summary of quote_spanned usage:
fn complete_guide_summary() {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β Aspect β quote! β quote_spanned! β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β Default span β call_site β Provided span β
// β Error location β Macro invocation β Specified location β
// β IDE support β Limited β Full β
// β Hygiene β Hygienic β Controlled β
// β Use case β Internal code β User-facing errors β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// quote! uses Span::call_site() for all generated tokens
// - Errors point to where macro was invoked
// - Fine for code that shouldn't produce errors
// quote_spanned!(span => ...) uses provided span
// - Errors point to specific user code location
// - Essential for good error messages
// Best practices:
// 1. Extract spans from user input tokens
// 2. Use most specific span possible (field > type > struct)
// 3. Use quote_spanned! for code that might error
// 4. Test error messages during development
}
// Key insight:
// quote_spanned is about developer experience. Without it, proc macros
// generate code that "belongs" to the macro itselfβthe compiler thinks
// errors originate from where the macro is defined. With quote_spanned,
// generated code "belongs" to the user's source codeβerrors point to
// the specific field, type, or item that caused the problem. This makes
// procedural macros usable in real projects where developers need clear
// error messages to fix their code.Key insight: quote_spanned! is essential for producing procedural macros that feel native to Rust. Without proper span handling, macros produce inscrutable error messages that point to macro internals rather than user code. By carefully extracting spans from input tokens and applying them to generated code, you ensure that type errors, borrow checker errors, and other diagnostics point to the right location in the user's source. This transforms the developer experience from "debugging the macro" to "fixing my code"βthe difference between a frustrating macro and a helpful one.
