comperr 0.1.0

A minimal, zero dependency 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_macro::{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_macro::{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)]

extern crate proc_macro;

use proc_macro::{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_macro::Span;
///
/// let e = Error::new(Span::call_site(), "something went wrong");
/// let ts = e.to_compile_error();
/// ```
pub struct Error {
    /// The source location the error should point at.
    span: Span,
    /// The message to display to the user.
    message: 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_macro::Span;
    ///
    /// let e = Error::new(Span::call_site(), "expected a string literal");
    /// ```
    pub fn new(span: Span, message: impl Into<String>) -> Self {
        Self {
            span,
            message: message.into(),
        }
    }

    /// 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_macro::{Span, TokenStream};
    ///
    /// pub fn my_macro(input: TokenStream) -> TokenStream {
    ///     let e = Error::new(Span::call_site(), "something went wrong");
    ///     e.to_compile_error()
    /// }
    /// ```
    pub fn to_compile_error(&self) -> TokenStream {
        let ident = Ident::new("compile_error", self.span);
        let mut bang = Punct::new('!', Spacing::Alone);
        bang.set_span(self.span);
        let mut lit = Literal::string(&self.message);
        lit.set_span(self.span);
        let mut group = Group::new(Delimiter::Parenthesis, TokenTree::from(lit).into());
        group.set_span(self.span);
        [
            TokenTree::Ident(ident),
            TokenTree::Punct(bang),
            TokenTree::Group(group),
        ]
        .into_iter()
        .collect()
    }
}

/// 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_macro::{Span, TokenStream};
///
/// pub fn my_macro(input: TokenStream) -> TokenStream {
///     return error(Span::call_site(), "something went wrong");
/// }
/// ```
pub fn error(span: Span, message: impl Into<String>) -> TokenStream {
    Error::new(span, message).to_compile_error()
}