use {
proc_macro2::{
Span,
TokenStream,
},
std::fmt,
thiserror::Error,
};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("Parse error: {0}")]
Parse(#[from] syn::Error),
#[error("Validation error: {message}")]
Validation {
message: String,
span: Span,
suggestion: Option<String>,
},
#[error("Resolution error: {message}")]
Resolution {
message: String,
span: Span,
available_types: Vec<String>,
},
#[error("Unsupported feature: {0}")]
Unsupported(#[from] UnsupportedFeature),
#[error("Internal error: {0}")]
Internal(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Error)]
pub enum UnsupportedFeature {
#[error("Const generic parameters are not supported in Kind definitions")]
ConstGenerics {
span: Span,
},
#[error("Verbatim bounds are not supported")]
VerbatimBounds {
span: Span,
},
#[error("Complex type not supported: {description}")]
ComplexTypes {
description: String,
span: Span,
},
#[error("Unsupported generic argument: {description}")]
GenericArgument {
description: String,
span: Span,
},
#[error("Unsupported bound type: {description}")]
BoundType {
description: String,
span: Span,
},
}
impl Error {
pub fn validation(
span: Span,
message: impl Into<String>,
) -> Self {
Self::Validation {
message: message.into(),
span,
suggestion: None,
}
}
pub fn with_suggestion(
mut self,
suggestion: impl Into<String>,
) -> Self {
if let Error::Validation {
suggestion: s, ..
} = &mut self
{
*s = Some(suggestion.into());
}
self
}
pub fn resolution(
span: Span,
message: impl Into<String>,
available_types: Vec<String>,
) -> Self {
Self::Resolution {
message: message.into(),
span,
available_types,
}
}
pub fn unsupported(
span: Span,
feature: impl Into<String>,
) -> Self {
Self::Unsupported(UnsupportedFeature::ComplexTypes {
description: feature.into(),
span,
})
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn span(&self) -> Span {
match self {
Error::Parse(e) => e.span(),
Error::Validation {
span, ..
} => *span,
Error::Resolution {
span, ..
} => *span,
Error::Unsupported(u) => u.span(),
Error::Internal(_) => Span::call_site(),
Error::Io(_) => Span::call_site(),
}
}
pub fn context(
self,
context: impl fmt::Display,
) -> Self {
match self {
Error::Internal(msg) => Error::Internal(format!("{context}: {msg}")),
Error::Validation {
message,
span,
suggestion,
} => Error::Validation {
message: format!("{context}: {message}"),
span,
suggestion,
},
Error::Resolution {
message,
span,
available_types,
} => Error::Resolution {
message: format!("{context}: {message}"),
span,
available_types,
},
Error::Parse(e) => {
let ctx_error = syn::Error::new(e.span(), format!("{context}: {e}"));
Error::Parse(ctx_error)
}
Error::Unsupported(u) => {
Error::Internal(format!("{context}: Unsupported feature: {u}"))
}
Error::Io(io) => Error::Internal(format!("{context}: I/O error: {io}")),
}
}
pub fn with_context(
self,
context: impl fmt::Display,
) -> Self {
self.context(context)
}
}
impl UnsupportedFeature {
pub fn span(&self) -> Span {
match self {
UnsupportedFeature::ConstGenerics {
span,
} => *span,
UnsupportedFeature::VerbatimBounds {
span,
} => *span,
UnsupportedFeature::ComplexTypes {
span, ..
} => *span,
UnsupportedFeature::GenericArgument {
span, ..
} => *span,
UnsupportedFeature::BoundType {
span, ..
} => *span,
}
}
}
impl From<Error> for syn::Error {
fn from(err: Error) -> Self {
if let Error::Parse(e) = err {
return e;
}
let span = err.span();
let mut message = err.to_string();
if let Error::Validation {
suggestion: Some(s), ..
} = &err
{
message = format!(
r#"{message}
help: {s}"#
);
}
if let Error::Resolution {
available_types, ..
} = &err && !available_types.is_empty()
{
message = format!(
r#"{message}
note: available alternatives: {}"#,
available_types.join(", ")
);
}
syn::Error::new(span, message)
}
}
pub struct ErrorCollector {
errors: Vec<syn::Error>,
}
#[allow(dead_code, reason = "API kept for completeness")]
impl ErrorCollector {
pub fn new() -> Self {
Self {
errors: Vec::new(),
}
}
pub fn push(
&mut self,
error: syn::Error,
) {
self.errors.push(error);
}
pub fn extend<I>(
&mut self,
other_errors: I,
) where
I: IntoIterator<Item = syn::Error>, {
self.errors.extend(other_errors);
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn into_errors(self) -> Vec<syn::Error> {
self.errors
}
pub fn iter(&self) -> std::slice::Iter<'_, syn::Error> {
self.errors.iter()
}
pub fn finish(self) -> syn::Result<()> {
if self.errors.is_empty() { Ok(()) } else { Err(Self::combine_errors(self.errors)) }
}
fn combine_errors(mut errors: Vec<syn::Error>) -> syn::Error {
let mut combined = errors.remove(0);
for err in errors {
combined.combine(err);
}
combined
}
}
impl Default for ErrorCollector {
fn default() -> Self {
Self::new()
}
}
impl IntoIterator for ErrorCollector {
type IntoIter = std::vec::IntoIter<syn::Error>;
type Item = syn::Error;
fn into_iter(self) -> Self::IntoIter {
self.errors.into_iter()
}
}
impl<'a> IntoIterator for &'a ErrorCollector {
type IntoIter = std::slice::Iter<'a, syn::Error>;
type Item = &'a syn::Error;
fn into_iter(self) -> Self::IntoIter {
self.errors.iter()
}
}
#[allow(dead_code, reason = "API kept for completeness")]
pub trait CollectErrors {
fn collect<F, T>(
&mut self,
f: F,
) -> Option<T>
where
F: FnOnce() -> syn::Result<T>;
fn collect_with_context<F, T>(
&mut self,
context: &str,
f: F,
) -> Option<T>
where
F: FnOnce() -> syn::Result<T>;
fn collect_our_result<F, T>(
&mut self,
f: F,
) -> Option<T>
where
F: FnOnce() -> Result<T>;
fn collect_our_result_with_context<F, T>(
&mut self,
context: &str,
f: F,
) -> Option<T>
where
F: FnOnce() -> Result<T>;
}
impl CollectErrors for ErrorCollector {
fn collect<F, T>(
&mut self,
f: F,
) -> Option<T>
where
F: FnOnce() -> syn::Result<T>, {
match f() {
Ok(value) => Some(value),
Err(e) => {
self.push(e);
None
}
}
}
fn collect_with_context<F, T>(
&mut self,
context: &str,
f: F,
) -> Option<T>
where
F: FnOnce() -> syn::Result<T>, {
match f() {
Ok(value) => Some(value),
Err(e) => {
let contextualized = syn::Error::new(e.span(), format!("{}: {}", context, e));
self.push(contextualized);
None
}
}
}
fn collect_our_result<F, T>(
&mut self,
f: F,
) -> Option<T>
where
F: FnOnce() -> Result<T>, {
match f() {
Ok(value) => Some(value),
Err(e) => {
self.push(e.into());
None
}
}
}
fn collect_our_result_with_context<F, T>(
&mut self,
context: &str,
f: F,
) -> Option<T>
where
F: FnOnce() -> Result<T>, {
match f() {
Ok(value) => Some(value),
Err(e) => {
let syn_err: syn::Error = e.into();
let contextualized =
syn::Error::new(syn_err.span(), format!("{}: {}", context, syn_err));
self.push(contextualized);
None
}
}
}
}
pub trait ToCompileError {
fn to_compile_error(self) -> TokenStream;
}
impl ToCompileError for Error {
fn to_compile_error(self) -> TokenStream {
let syn_error: syn::Error = self.into();
syn_error.to_compile_error()
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "Tests use panicking operations for brevity and clarity")]
mod tests {
use super::*;
#[test]
fn test_error_span() {
let span = Span::call_site();
let err = Error::validation(span, "test message");
assert_eq!(format!("{:?}", err.span()), format!("{:?}", span), "Span should be preserved");
}
#[test]
fn test_validation_error() {
let span = Span::call_site();
let err = Error::validation(span, "invalid input");
assert!(err.to_string().contains("invalid input"));
}
#[test]
fn test_validation_error_with_suggestion() {
let span = Span::call_site();
let err = Error::validation(span, "invalid input").with_suggestion("try this instead");
let syn_err: syn::Error = err.into();
let err_str = syn_err.to_string();
eprintln!("Error string: '{err_str}'");
eprintln!("Contains 'invalid input': {}", err_str.contains("invalid input"));
eprintln!("Contains 'try this instead': {}", err_str.contains("try this instead"));
assert!(err_str.contains("invalid input"));
assert!(err_str.contains("try this instead"));
}
#[test]
fn test_resolution_error() {
let span = Span::call_site();
let err = Error::resolution(span, "cannot resolve", vec!["Type1".to_string()]);
assert!(err.to_string().contains("cannot resolve"));
}
#[test]
fn test_unsupported_const_generics() {
let span = Span::call_site();
let err = UnsupportedFeature::ConstGenerics {
span,
};
assert!(err.to_string().contains("Const generic parameters are not supported"));
}
#[test]
fn test_error_context() {
let err = Error::internal("original message");
let err_with_context = err.context("while processing");
assert!(err_with_context.to_string().contains("while processing: original message"));
}
#[test]
fn test_syn_error_conversion() {
let span = Span::call_site();
let err = Error::validation(span, "test error");
let syn_err: syn::Error = err.into();
assert!(syn_err.to_string().contains("test error"));
}
#[test]
fn test_resolution_error_with_available_types() {
let span = Span::call_site();
let err = Error::resolution(
span,
"cannot find type",
vec!["String".to_string(), "Vec".to_string()],
);
let syn_err: syn::Error = err.into();
let err_string = syn_err.to_string();
assert!(err_string.contains("cannot find type"));
}
#[test]
fn test_collect_success() {
let mut errors = ErrorCollector::new();
let result = errors.collect(|| Ok::<_, syn::Error>(42));
assert_eq!(result, Some(42));
assert!(errors.is_empty());
}
#[test]
fn test_collect_error() {
let mut errors = ErrorCollector::new();
let result =
errors.collect(|| Err::<i32, _>(syn::Error::new(Span::call_site(), "test error")));
assert_eq!(result, None);
assert_eq!(errors.len(), 1);
}
#[test]
fn test_collect_with_context() {
let mut errors = ErrorCollector::new();
let result = errors.collect_with_context("parsing", || {
Err::<i32, _>(syn::Error::new(Span::call_site(), "failed"))
});
assert_eq!(result, None);
assert_eq!(errors.len(), 1);
let combined_err = errors.finish().unwrap_err();
assert!(combined_err.to_string().contains("parsing"));
assert!(combined_err.to_string().contains("failed"));
}
#[test]
fn test_collect_our_result() {
let mut errors = ErrorCollector::new();
let result = errors.collect_our_result(|| Ok::<_, Error>(100));
assert_eq!(result, Some(100));
assert!(errors.is_empty());
}
#[test]
fn test_collect_our_result_error() {
let mut errors = ErrorCollector::new();
let result = errors.collect_our_result(|| {
Err::<i32, _>(Error::validation(Span::call_site(), "validation failed"))
});
assert_eq!(result, None);
assert_eq!(errors.len(), 1);
}
#[test]
fn test_collect_our_result_with_context() {
let mut errors = ErrorCollector::new();
let result = errors.collect_our_result_with_context("in function", || {
Err::<i32, _>(Error::validation(Span::call_site(), "bad value"))
});
assert_eq!(result, None);
assert_eq!(errors.len(), 1);
let combined_err = errors.finish().unwrap_err();
assert!(combined_err.to_string().contains("in function"));
assert!(combined_err.to_string().contains("bad value"));
}
#[test]
fn test_multiple_collects() {
let mut errors = ErrorCollector::new();
let r1 = errors.collect(|| Ok::<_, syn::Error>(1));
let r2 = errors.collect(|| Err::<i32, _>(syn::Error::new(Span::call_site(), "error 1")));
let r3 = errors.collect(|| Ok::<_, syn::Error>(3));
let r4 = errors.collect(|| Err::<i32, _>(syn::Error::new(Span::call_site(), "error 2")));
assert_eq!(r1, Some(1));
assert_eq!(r2, None);
assert_eq!(r3, Some(3));
assert_eq!(r4, None);
assert_eq!(errors.len(), 2);
let combined_err = errors.finish().unwrap_err();
let compile_err_str = combined_err.to_compile_error().to_string();
assert!(compile_err_str.contains("error 1"));
assert!(compile_err_str.contains("error 2"));
}
#[test]
fn test_error_collector_methods() {
let mut errors = ErrorCollector::new();
assert!(errors.is_empty());
assert!(!errors.has_errors());
assert_eq!(errors.len(), 0);
errors.push(syn::Error::new(Span::call_site(), "error 1"));
assert!(!errors.is_empty());
assert!(errors.has_errors());
assert_eq!(errors.len(), 1);
errors.push(syn::Error::new(Span::call_site(), "error 2"));
assert_eq!(errors.len(), 2);
}
#[test]
fn test_to_compile_error() {
let err = Error::validation(Span::call_site(), "test error");
let token_stream = err.to_compile_error();
let output = token_stream.to_string();
assert!(!output.is_empty());
}
}