statum-macros 0.8.7

Proc macros for representing legal workflow and protocol states explicitly in Rust
Documentation
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote_spanned};
use syn::spanned::Spanned;
use syn::{Item, LitStr};

#[derive(Clone, Debug)]
pub(crate) struct DiagnosticMessage {
    summary: String,
    sections: Vec<(&'static str, String)>,
}

impl DiagnosticMessage {
    pub(crate) fn new(summary: impl Into<String>) -> Self {
        Self {
            summary: summary.into(),
            sections: Vec::new(),
        }
    }

    pub(crate) fn found(self, value: impl Into<String>) -> Self {
        self.section("Found", value)
    }

    pub(crate) fn expected(self, value: impl Into<String>) -> Self {
        self.section("Expected", value)
    }

    pub(crate) fn fix(self, value: impl Into<String>) -> Self {
        self.section("Fix", value)
    }

    pub(crate) fn reason(self, value: impl Into<String>) -> Self {
        self.section("Reason", value)
    }

    pub(crate) fn note(self, value: impl Into<String>) -> Self {
        self.section("Note", value)
    }

    pub(crate) fn candidates(self, value: impl Into<String>) -> Self {
        self.section("Candidates", value)
    }

    pub(crate) fn assumption(self, value: impl Into<String>) -> Self {
        self.section("Assumption", value)
    }

    pub(crate) fn help(self, value: impl Into<String>) -> Self {
        self.section("Help", value)
    }

    pub(crate) fn section(mut self, label: &'static str, value: impl Into<String>) -> Self {
        let value = value.into();
        if !value.trim().is_empty() {
            self.sections.push((label, value));
        }
        self
    }

    pub(crate) fn render(&self) -> String {
        let mut rendered = format!("Error: {}", self.summary);
        for (label, value) in &self.sections {
            rendered.push('\n');
            rendered.push_str(&render_section(label, value));
        }
        rendered
    }
}

pub(crate) fn compile_error_at(span: Span, message: &DiagnosticMessage) -> TokenStream {
    let rendered = LitStr::new(&message.render(), span);
    quote_spanned! { span =>
        compile_error!(#rendered);
    }
}

pub(crate) fn error_at(span: Span, message: &DiagnosticMessage) -> syn::Error {
    syn::Error::new(span, message.render())
}

pub(crate) fn error_spanned<T>(node: &T, message: &DiagnosticMessage) -> syn::Error
where
    T: Spanned + ToTokens,
{
    syn::Error::new_spanned(node, message.render())
}

pub(crate) fn compact_display<T>(value: &T) -> String
where
    T: ToTokens + ?Sized,
{
    normalize_tokens(&value.to_token_stream().to_string())
}

pub(crate) fn compact_text(value: &str) -> String {
    normalize_tokens(value)
}

pub(crate) fn item_signature(item: &Item) -> String {
    match item {
        Item::Const(item) => format!("`const {}: ... = ...;`", item.ident),
        Item::Enum(item) => format!(
            "`enum {}{} {{ ... }}`",
            item.ident,
            generics_suffix(&compact_display(&item.generics))
        ),
        Item::ExternCrate(item) => format!("`extern crate {};`", item.ident),
        Item::Fn(item) => format!("`{} {{ ... }}`", compact_display(&item.sig)),
        Item::ForeignMod(_) => "`extern \"...\" { ... }`".to_string(),
        Item::Impl(item) => {
            let trait_prefix = item
                .trait_
                .as_ref()
                .map(|(_, path, _)| format!("{} for ", compact_display(path)))
                .unwrap_or_default();
            format!(
                "`impl {}{} {{ ... }}`",
                trait_prefix,
                compact_display(&item.self_ty)
            )
        }
        Item::Macro(item) => format!("`{}! {{ ... }}`", compact_display(&item.mac.path)),
        Item::Mod(item) => format!("`mod {} {{ ... }}`", item.ident),
        Item::Static(item) => format!("`static {}: ... = ...;`", item.ident),
        Item::Struct(item) => format!(
            "`struct {}{} {{ ... }}`",
            item.ident,
            generics_suffix(&compact_display(&item.generics))
        ),
        Item::Trait(item) => format!(
            "`trait {}{} {{ ... }}`",
            item.ident,
            generics_suffix(&compact_display(&item.generics))
        ),
        Item::TraitAlias(item) => format!(
            "`trait {}{} = ...;`",
            item.ident,
            generics_suffix(&compact_display(&item.generics))
        ),
        Item::Type(item) => format!(
            "`type {}{} = ...;`",
            item.ident,
            generics_suffix(&compact_display(&item.generics))
        ),
        Item::Union(item) => format!(
            "`union {}{} {{ ... }}`",
            item.ident,
            generics_suffix(&compact_display(&item.generics))
        ),
        Item::Use(item) => format!("`use {};`", compact_display(&item.tree)),
        _ => format!("`{}`", compact_display(item)),
    }
}

fn render_section(label: &str, value: &str) -> String {
    let indent = " ".repeat(label.len() + 2);
    let mut lines = value.lines();
    let Some(first) = lines.next() else {
        return format!("{label}:");
    };

    let mut rendered = format!("{label}: {first}");
    for line in lines {
        rendered.push('\n');
        rendered.push_str(&indent);
        rendered.push_str(line);
    }
    rendered
}

fn normalize_tokens(tokens: &str) -> String {
    let mut normalized = tokens.to_owned();
    for (from, to) in [
        (" :: ", "::"),
        (":: ", "::"),
        (" ::", "::"),
        (" < ", "<"),
        ("< ", "<"),
        (" >", ">"),
        (" ( ", "("),
        ("( ", "("),
        (" )", ")"),
        (" [ ", "["),
        ("[ ", "["),
        (" ]", "]"),
        (" { ", " { "),
        (" ;", ";"),
        (" , ", ", "),
        (" : ", ": "),
        ("< '", "<'"),
        ("& '", "&'"),
        (" & ", " &"),
    ] {
        normalized = normalized.replace(from, to);
    }

    while normalized.contains("> >") {
        normalized = normalized.replace("> >", ">>");
    }
    while normalized.contains("  ") {
        normalized = normalized.replace("  ", " ");
    }

    normalized.trim().to_string()
}

fn generics_suffix(generics: &str) -> String {
    if generics.is_empty() || generics == "<>" {
        String::new()
    } else {
        generics.to_string()
    }
}