use proc_macro2::Span;
#[derive(Debug)]
pub struct ErrorDetails {
pub span: Span,
pub message: String,
pub help: Option<String>,
pub hints: Option<String>,
}
impl ErrorDetails {
pub fn new<S: AsRef<str>>(span: Span, message: S) -> Self {
Self {
span,
message: message.as_ref().to_string(),
help: None,
hints: None,
}
}
#[allow(dead_code)]
pub fn with_help<S: AsRef<str>>(mut self, help: S) -> Self {
self.help = Some(help.as_ref().to_string());
self
}
#[allow(dead_code)]
pub fn with_hints<S: AsRef<str>>(mut self, hints: S) -> Self {
self.hints = Some(hints.as_ref().to_string());
self
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum Error {
Unknown(Span),
Parser(Span),
ParseItemNotStruct(Span),
MissingFieldIdentifier(Span),
EmptyTypePath(Span),
UnsupportedFieldType(Span, String),
ReservedKeywordFieldName(Span, String, String),
UnknownFieldType(Span, String),
MissingRequiredSpanField(Span),
InvalidFieldNamingConvention(Span, String, String),
InvalidFieldType(Span),
InvalidFieldTypeMessage(Span, String),
InvalidPropsArgNotNamedIdent(Span),
InvalidPropsFoundSelf(Span),
UseStateExpectsTuple(Span),
UseStateMalformedSetName(Span, String, String),
ComponentExprExpectedPropsStruct(Span),
ComponentExprUnexpectedToken(Span),
}
impl Error {
pub fn get(&self) -> ErrorDetails {
match self {
| Self::Unknown(span) => ErrorDetails::new(*span, "unknown error"),
| Self::Parser(span) => {
ErrorDetails {
span: *span,
message: "failed to parse the syntax tree".to_string(),
help: Some(
"Check that your Rust syntax is valid and the struct is well-formed".to_string(),
),
hints: None,
}
},
| Self::ParseItemNotStruct(span) => {
ErrorDetails {
span: *span,
message: "#[laburnum_syntax] can only be applied to structs".to_string(),
help: Some(
"This attribute is designed to work with struct definitions only".to_string(),
),
hints: Some(
"Try using: #[laburnum_syntax(AST)] pub struct MyStruct { ... }".to_string(),
),
}
},
| Self::MissingFieldIdentifier(span) => {
ErrorDetails {
span: *span,
message: "struct field must have a name".to_string(),
help: Some(
"All fields in structs used with #[laburnum_syntax] must be named fields".to_string(),
),
hints: Some(
"Example: pub struct MyStruct { field_name: NodeId<crate::Type> }".to_string(),
),
}
},
| Self::EmptyTypePath(span) => {
ErrorDetails {
span: *span,
message: "type path cannot be empty".to_string(),
help: Some(
"Type paths must contain at least one segment (e.g., `NodeId`, `Option`, `Vec`)".to_string(),
),
hints: Some(
"Check that your field type is properly specified".to_string(),
),
}
},
| Self::UnsupportedFieldType(span, type_name) => {
ErrorDetails {
span: *span,
message: format!("unsupported field type: {type_name}"),
help: Some(
"Fields must use supported container types like NodeId, Field, Option, Vec, or EnumNodeId".to_string(),
),
hints: Some(
"Supported types: NodeId<T>, Field<T>, Option<NodeId<T>>, Vec<T>, EnumNodeId<T>".to_string(),
),
}
},
| Self::ReservedKeywordFieldName(span, field_name, keyword) => {
ErrorDetails {
span: *span,
message: format!(
"field name '{field_name}' converts to reserved keyword '{keyword}' when converted to UpperCamelCase"
),
help: Some(
"Choose a different field name that doesn't conflict with Rust keywords when converted to UpperCamelCase".to_string(),
),
hints: Some(
"Try using a different name like 'my_field' instead of 'self_'".to_string(),
),
}
},
| Self::UnknownFieldType(span, field_type) => {
ErrorDetails {
span: *span,
message: format!("unknown field type: {field_type}"),
help: Some(
"This field type is not supported by the laburnum_syntax macro".to_string(),
),
hints: Some(
"Supported field types: NodeId<T>, Field<T>, Option<T>, Vec<T>, EnumNodeId<T>, String".to_string(),
),
}
},
| Self::MissingRequiredSpanField(span) => {
ErrorDetails {
span: *span,
message: "CST structs must have a 'span' field of type 'Span'".to_string(),
help: Some(
"Add a 'span: Span' field to your CST struct".to_string(),
),
hints: Some(
"Example: pub struct MyCst { span: Span, /* other fields */ }".to_string(),
),
}
},
| Self::InvalidFieldNamingConvention(span, field_name, expected_suffix) => {
ErrorDetails {
span: *span,
message: format!(
"field '{field_name}' must end with '{expected_suffix}' for this field type"
),
help: Some(
"CST field naming conventions require specific suffixes based on field type".to_string(),
),
hints: Some(
format!("Rename the field to '{field_name}{expected_suffix}' or similar"),
),
}
},
| Self::InvalidFieldType(span) => {
ErrorDetails {
span: *span,
message: "invalid field type - fields must use NodeId, Field, Vec, Option, or other supported containers".to_string(),
help: Some(
"Supported field types include:\n - NodeId<T>\n - Field<T>\n - Option<NodeId<T>>\n - Vec<T>\n - EnumNodeId<T>".to_string(),
),
hints: Some(
"Example: field: NodeId<crate::Type> or field: Option<NodeId<crate::Type>>".to_string(),
),
}
},
| Self::InvalidFieldTypeMessage(span, message) => {
ErrorDetails {
span: *span,
message: message.to_string(),
help: None,
hints: None,
}
},
| Self::InvalidPropsArgNotNamedIdent(span) => {
ErrorDetails {
span: *span,
message: "argument is not a named identifier".to_string(),
help: Some("Arguments must be simple named identifiers".to_string()),
hints: Some("Example: fn my_function(arg_name: Type) { ... }".to_string()),
}
},
| Self::InvalidPropsFoundSelf(span) => {
ErrorDetails {
span: *span,
message: "Found self argument which is not allowed".to_string(),
help: Some(
"The #[laburnum_syntax] macro generates methods that already have self. Remove any self parameter from your struct definition.".to_string(),
),
hints: None,
}
},
| Self::UseStateExpectsTuple(span) => {
ErrorDetails {
span: *span,
message: "use_state expects a tuple pattern for destructuring".to_string(),
help: Some(
"Use tuple destructuring to get both the state value and setter function".to_string(),
),
hints: Some(
"Example: let (state, set_state) = use_state(initial_value);".to_string(),
),
}
},
| Self::UseStateMalformedSetName(span, expected, got) => {
ErrorDetails {
span: *span,
message: format!(
"use_state setter function should be named `{expected}` but found `{got}`"
),
help: Some(format!("Rename the second tuple element to `{expected}`")),
hints: Some(
"The setter follows the convention set_<state_name>".to_string(),
),
}
},
| Self::ComponentExprExpectedPropsStruct(span) => {
ErrorDetails {
span: *span,
message: "expected a props struct but found something else".to_string(),
help: Some(
"Component expressions require a props struct to pass data".to_string(),
),
hints: Some(
"Example: MyComponent { prop1: value1, prop2: value2 }".to_string(),
),
}
},
| Self::ComponentExprUnexpectedToken(span) => {
ErrorDetails {
span: *span,
message: "unexpected token in component expression".to_string(),
help: Some(
"Check the syntax of your component expression".to_string(),
),
hints: Some(
"Component expressions should follow the pattern: ComponentName { props }".to_string(),
),
}
},
}
}
}
#[derive(Debug, Default)]
pub struct ErrorAccumulator {
errors: Vec<syn::Error>,
}
#[allow(dead_code)]
impl ErrorAccumulator {
pub fn new() -> Self {
Self { errors: Vec::new() }
}
pub fn add_error(&mut self, error: Error) {
let details = error.get();
let mut syn_error = syn::Error::new(details.span, &details.message);
if let Some(help) = &details.help {
let help_error = syn::Error::new(details.span, format!("help: {help}"));
syn_error.combine(help_error);
}
if let Some(hints) = &details.hints {
let hints_error = syn::Error::new(details.span, format!("hint: {hints}"));
syn_error.combine(hints_error);
}
self.errors.push(syn_error);
}
pub fn add_syn_error(&mut self, error: syn::Error) {
self.errors.push(error);
}
pub fn add_spanned_error<T>(
&mut self,
tokens: T,
message: impl std::fmt::Display,
) where
T: quote::ToTokens,
{
let error = syn::Error::new_spanned(tokens, message);
self.errors.push(error);
}
pub fn add_simple_error(
&mut self,
span: proc_macro2::Span,
message: impl std::fmt::Display,
) {
let error = syn::Error::new(span, message);
self.errors.push(error);
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn error_count(&self) -> usize {
self.errors.len()
}
pub fn into_result<T>(self, ok_value: T) -> Result<T, syn::Error> {
if self.errors.is_empty() {
Ok(ok_value)
} else {
Err(self.combine_errors())
}
}
pub fn into_error_result(self) -> Result<(), syn::Error> {
if self.errors.is_empty() {
Ok(())
} else {
Err(self.combine_errors())
}
}
fn combine_errors(self) -> syn::Error {
let mut errors = self.errors.into_iter();
let mut combined = errors.next().expect("Should have at least one error");
for error in errors {
combined.combine(error);
}
combined
}
pub fn extend(&mut self, other: ErrorAccumulator) {
self.errors.extend(other.errors);
}
pub fn with_error(error: Error) -> Self {
let mut acc = Self::new();
acc.add_error(error);
acc
}
pub fn create_detailed_error(error: Error) -> syn::Error {
let details = error.get();
let mut message = details.message.clone();
if let Some(help) = &details.help {
message.push_str(&format!("\nhelp: {help}"));
}
if let Some(hints) = &details.hints {
message.push_str(&format!("\nnote: {hints}"));
}
syn::Error::new(details.span, message)
}
pub fn add_enhanced_error(&mut self, error: Error) {
let enhanced_error = Self::create_detailed_error(error);
self.errors.push(enhanced_error);
}
}