use syn::{Ident, spanned::Spanned};
pub fn tag_mismatch_error(closing_name: &Ident, opening_name: &Ident) -> syn::Error {
syn::Error::new_spanned(
closing_name,
format!(
"Closing tag `</{closing_name}>` does not match opening tag `<{opening_name}>`. \
Tags must be properly nested.\n\
\x20 help: Change the closing tag to `</{opening_name}>`\n\
\x20 note: RSX syntax requires matching tags like in HTML/JSX"
),
)
}
pub fn unclosed_tag_error(span: proc_macro2::Span, tag_name: &Ident) -> syn::Error {
syn::Error::new(
span,
format!(
"Unclosed tag `<{tag_name}>`. Expected closing tag before end of input.\n\
\x20 help: Add a closing tag `</{tag_name}>`\n\
\x20 note: All RSX tags must be properly closed"
),
)
}
pub fn unclosed_fragment_error(span: proc_macro2::Span) -> syn::Error {
syn::Error::new(
span,
"Unclosed fragment `<>`. Expected closing tag `</>` before end of input.\n\
\x20 help: Add a closing tag `</>`\n\
\x20 note: Fragments must be properly closed",
)
}
pub fn invalid_child_in_tag_error(span: proc_macro2::Span, tag_name: &Ident) -> syn::Error {
syn::Error::new(
span,
format!(
"Unexpected token in `<{tag_name}>`. \
Expected one of: {{expr}}, \"text\", <child>, or </{tag_name}>\n\
\x20 help: RSX children must be expressions in {{}}, text in quotes, or nested elements\n\
\x20 note: Bare identifiers are not allowed - wrap them in braces like {{variable}}"
),
)
}
pub fn invalid_child_in_fragment_error(span: proc_macro2::Span) -> syn::Error {
syn::Error::new(
span,
"Unexpected token in fragment. Expected one of: {expr}, \"text\", <child>, or </>\n\
\x20 help: RSX children must be expressions in {}, text in quotes, or nested elements",
)
}
pub fn for_loop_missing_brace_error(span: proc_macro2::Span) -> syn::Error {
syn::Error::new(
span,
"Expected '{' after for-in expression to start the loop body.\n\
\x20 help: Add a block like: for item in items { <li>{item}</li> }\n\
\x20 note: The for loop syntax is: for pattern in expression { body }",
)
}
pub fn for_loop_invalid_body_error(span: proc_macro2::Span) -> syn::Error {
syn::Error::new(
span,
"Unexpected token in for-loop body. Expected element, expression, or spread.\n\
\x20 help: For-loop bodies must contain RSX elements like <div> or expressions like {item}\n\
\x20 note: Example: for item in items { <li>{item}</li> }",
)
}
pub fn condition_tuple_wrong_count_error<T: Spanned>(
tuple: &T,
attr_name: &str,
found_count: usize,
) -> syn::Error {
syn::Error::new(
tuple.span(),
format!(
"The `{attr_name}` attribute expects exactly 2 values, found {found_count}.\n\
\x20 help: Use the format: {attr_name}={{(condition, |el| el.method())}}\n\
\x20 note: The first value is the condition, the second is a closure that modifies the element"
),
)
}
pub fn for_loop_missing_key_error(tag_name: &Ident) -> syn::Error {
syn::Error::new_spanned(
tag_name,
format!(
"Element `<{tag_name}>` inside a for-loop has event handlers but no `id` or `key` attribute.\n\
\x20 help: Add a `key={{unique_value}}` attribute so each iteration gets a unique ID:\n\
\x20 for item in &self.items {{ <{tag_name} key={{item.id}} onClick={{...}}>...</{tag_name}> }}\n\
\x20 note: Elements in loops share the same source location, so the auto-generated ID\n\
\x20 would be identical across iterations, causing GPUI state conflicts.\n\
\x20 Use `key` for a composite auto-ID, or `id` to supply a fully custom ID."
),
)
}
pub fn condition_tuple_wrong_type_error<T: Spanned>(value: &T, attr_name: &str) -> syn::Error {
syn::Error::new(
value.span(),
format!(
"The `{attr_name}` attribute expects a tuple of (condition, closure).\n\
\x20 help: Use the format: {attr_name}={{(condition, |el| el.method())}}\n\
\x20 note: Example: when={{(is_active, |el| el.bg(rgb(0x00ff00)))}}"
),
)
}
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::Span;
fn make_ident(name: &str) -> Ident {
Ident::new(name, Span::call_site())
}
#[test]
fn tag_mismatch_contains_both_tag_names() {
let opening = make_ident("div");
let closing = make_ident("span");
let err = tag_mismatch_error(&closing, &opening);
let msg = err.to_string();
assert!(msg.contains("div"), "应包含开启标签名 div");
assert!(msg.contains("span"), "应包含关闭标签名 span");
}
#[test]
fn tag_mismatch_contains_help_hint() {
let opening = make_ident("section");
let closing = make_ident("div");
let err = tag_mismatch_error(&closing, &opening);
let msg = err.to_string();
assert!(msg.contains("help:"), "应包含 help 提示");
assert!(msg.contains("note:"), "应包含 note 提示");
}
#[test]
fn unclosed_tag_contains_tag_name() {
let tag = make_ident("nav");
let err = unclosed_tag_error(Span::call_site(), &tag);
let msg = err.to_string();
assert!(msg.contains("nav"), "应包含未闭合标签名");
assert!(msg.contains("help:"), "应包含 help 提示");
}
#[test]
fn unclosed_fragment_contains_help() {
let err = unclosed_fragment_error(Span::call_site());
let msg = err.to_string();
assert!(msg.contains("</>"), "应提示关闭 Fragment");
assert!(msg.contains("help:"), "应包含 help 提示");
}
#[test]
fn invalid_child_in_tag_contains_tag_and_hint() {
let tag = make_ident("ul");
let err = invalid_child_in_tag_error(Span::call_site(), &tag);
let msg = err.to_string();
assert!(msg.contains("ul"), "应包含父标签名");
assert!(msg.contains("help:"), "应包含 help 提示");
}
#[test]
fn for_loop_missing_brace_has_example() {
let err = for_loop_missing_brace_error(Span::call_site());
let msg = err.to_string();
assert!(msg.contains("for"), "应提及 for 循环");
assert!(msg.contains("help:"), "应包含 help 提示");
}
#[test]
fn condition_tuple_wrong_count_shows_found_and_expected() {
let tokens: proc_macro2::TokenStream = "(a, b, c)".parse().unwrap();
let expr: syn::ExprTuple = syn::parse2(tokens).unwrap();
let err = condition_tuple_wrong_count_error(&expr, "when", 3);
let msg = err.to_string();
assert!(msg.contains("when"), "应包含属性名");
assert!(msg.contains('3'), "应包含实际元素数");
assert!(msg.contains("help:"), "应包含 help 提示");
}
#[test]
fn condition_tuple_wrong_type_contains_attr_name() {
let tokens: proc_macro2::TokenStream = "true".parse().unwrap();
let expr: syn::Expr = syn::parse2(tokens).unwrap();
let err = condition_tuple_wrong_type_error(&expr, "whenSome");
let msg = err.to_string();
assert!(msg.contains("whenSome"), "应包含属性名");
assert!(msg.contains("help:"), "应包含 help 提示");
}
}