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 struct

Default 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 here

The 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.