comperr 0.2.0

A minimal, lightweight crate for emitting span-accurate compile-time errors from procedural macros.
Documentation
//! # comperr
//!
//! A minimal, zero dependency crate for emitting span-accurate compile-time
//! errors from procedural macros.
//!
//! ## Overview
//!
//! When writing proc-macros, you often need to emit a `compile_error!` that
//! points at a specific location in the user's source code, but you don't want to
//! extremely bloat compilation time by pulling a massive crate.
//!
//! The naive approach of formatting a string and calling `.parse()` loses the span entirely,
//! because tokens born from a string have no source location. `comperr`
//! constructs the error `TokenStream` token by token, attaching the correct
//! [`Span`] to each one so the compiler diagnostic lands exactly where you
//! want it.
//!
//! ## Usage
//!
//! One-shot with the free function:
//!
//! ```ignore
//! use proc_macro2::{Span, TokenStream};
//!
//! pub fn my_macro(input: TokenStream) -> TokenStream {
//!     return comperr::error(Span::call_site(), "something went wrong");
//! }
//! ```
//!
//! Or using the [`Error`] struct directly:
//!
//! ```ignore
//! use proc_macro2::{Span, TokenStream};
//!
//! pub fn my_macro(input: TokenStream) -> TokenStream {
//!     let e = comperr::Error::new(Span::call_site(), "something went wrong");
//!     return e.to_compile_error();
//! }
//! ```
//!
//! ## How It Works
//!
//! Every token in a `TokenStream` carries a [`Span`] that tells the compiler
//! where in the source code that token came from. When `compile_error!` is
//! invoked, the compiler reads the span off the tokens it receives and uses
//! that to place the diagnostic. By constructing each token manually and
//! calling `.set_span()` on it, the resulting error points at the original
//! source location rather than at a meaningless internal position.
//!
//! ## Performance
//!
//! All work happens at compile time during macro expansion.
//! There is no runtime overhead in your final binary.

#![warn(missing_docs)]

use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};

/// A compile-time error tied to a source location.
///
/// Construct one with [`Error::new`] and convert it to a [`TokenStream`] with
/// [`Error::to_compile_error`], or use the [`error`] free function for the
/// common one-shot case.
///
/// # Example
///
/// ```ignore
/// use comperr::Error;
/// use proc_macro2::Span;
///
/// let e = Error::new(Span::call_site(), "something went wrong");
/// let ts = e.to_compile_error();
/// ```
pub struct Error {
    inner: Option<(Span, String)>,
    overflow: Option<Vec<(Span, String)>>,
}

impl Error {
    /// Creates a new [`Error`] at the given span with the given message.
    ///
    /// The `message` can be any type that implements [`Into<String>`], so both
    /// `&str` and `String` work naturally.
    ///
    /// # Arguments
    ///
    /// - `span`: The source location the error should point at. This is
    ///   typically obtained from a token in the macro input, for example via
    ///   a `Literal::span()` call.
    /// - `message`: A human-readable description of what went wrong.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use comperr::Error;
    /// use proc_macro2::Span;
    ///
    /// let e = Error::new(Span::call_site(), "expected a string literal");
    /// ```
    #[inline]
    pub fn new(span: Span, message: impl Into<String>) -> Self {
        Self {
            inner: Some((span, message.into())),
            overflow: None,
        }
    }

    /// Converts the error into a [`TokenStream`] containing a `compile_error!`
    /// invocation with the span attached to every token.
    ///
    /// When this [`TokenStream`] is returned from a proc-macro, the Rust
    /// compiler emits a compile error pointing at the original source location
    /// recorded in the span.
    ///
    /// # How the Span Is Preserved
    ///
    /// Each token in the resulting `compile_error!(...)` invocation has its
    /// span set explicitly via `.set_span()`. The tokens carry the span as
    /// attached metadata, and `compile_error!` reads that metadata to place
    /// the diagnostic. This is why the error points at the right location
    /// rather than at an internal or meaningless position.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use comperr::Error;
    /// use proc_macro2::{Span, TokenStream};
    ///
    /// pub fn my_macro(input: TokenStream) -> TokenStream {
    ///     let e = Error::new(Span::call_site(), "something went wrong");
    ///     e.to_compile_error()
    /// }
    /// ```
    #[must_use]
    #[inline]
    pub fn to_compile_error(&self) -> TokenStream {
        let mut tokens = TokenStream::new();
        let iter = self.inner.iter().chain(self.overflow.iter().flatten());
        for (span, msg) in iter {
            let ident = Ident::new("compile_error", *span);
            let mut bang = Punct::new('!', Spacing::Alone);
            bang.set_span(*span);
            let mut lit = Literal::string(msg);
            lit.set_span(*span);
            let mut group = Group::new(Delimiter::Parenthesis, TokenTree::from(lit).into());
            group.set_span(*span);
            let mut semi = Punct::new(';', Spacing::Alone);
            semi.set_span(*span);
            tokens.extend([
                TokenTree::Ident(ident),
                TokenTree::Punct(bang),
                TokenTree::Group(group),
                TokenTree::Punct(semi),
            ]);
        }
        tokens
    }

    /// Combines another error with this one.
    ///
    /// When `to_compile_error()` is called, both errors will be emitted together.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use comperr::Error;
    /// use proc_macro2::Span;
    ///
    /// let mut e1 = Error::new(Span::call_site(), "first error");
    /// let e2 = Error::new(Span::call_site(), "second error");
    /// e1.combine(e2);
    /// ```
    #[inline]
    pub fn combine(&mut self, mut other: Self) {
        match (&mut self.inner, other.inner.take()) {
            (Some(_), Some(msg)) => {
                self.overflow.get_or_insert_with(Vec::new).push(msg);
            }
            (inner @ None, Some(msg)) => {
                *inner = Some(msg);
            }
            (_, None) => {}
        }
        if let Some(v) = other.overflow.take() {
            let out = self.overflow.get_or_insert_with(Vec::new);
            out.extend(v);
        }
    }
}

/// Emits a span-accurate compile-time error in one call.
///
/// This is a convenience wrapper around [`Error::new`] and
/// [`Error::to_compile_error`]. Use this when you need to emit a single error
/// and return immediately. For more complex cases, use [`Error`] directly.
///
/// # Arguments
///
/// - `span`: The source location the error should point at.
/// - `message`: A human-readable description of what went wrong.
///
/// # Example
///
/// ```ignore
/// use comperr::error;
/// use proc_macro2::{Span, TokenStream};
///
/// pub fn my_macro(input: TokenStream) -> TokenStream {
///     return error(Span::call_site(), "something went wrong");
/// }
/// ```
#[inline]
pub fn error(span: Span, message: impl Into<String>) -> TokenStream {
    Error::new(span, message).to_compile_error()
}