use proc_macro2::Span;
use syn::spanned::Spanned;
use syn::visit_mut::{self, VisitMut};
use syn::{Expr, ExprCall, ExprClosure, ExprForLoop, ExprIf, ExprLoop, ExprMatch, ExprWhile};
pub struct HookValidator {
branch_depth: usize,
errors: Vec<syn::Error>,
}
impl HookValidator {
pub fn new() -> Self {
Self {
branch_depth: 0,
errors: Vec::new(),
}
}
fn is_branched(&self) -> bool {
self.branch_depth > 0
}
fn with_branch<F, O>(&mut self, f: F) -> O
where
F: FnOnce(&mut Self) -> O,
{
self.branch_depth += 1;
let result = f(self);
self.branch_depth -= 1;
result
}
fn is_hook_call(func: &Expr) -> Option<&syn::Path> {
if let Expr::Path(path_expr) = func
&& let Some(segment) = path_expr.path.segments.last()
&& segment.ident.to_string().starts_with("use_")
{
return Some(&path_expr.path);
}
None
}
fn validate_hook_call(&mut self, path: &syn::Path, span: Span) {
if self.is_branched() {
let hook_name = path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_else(|| "hook".to_string());
let error = syn::Error::new(
span,
format!(
"Hook `{}` cannot be called inside a conditional, loop, or nested function.\n\
\n\
Rules of Hooks:\n\
1. Only call hooks at the top level of your component\n\
2. Don't call hooks inside loops, conditions, or nested functions\n\
3. Hooks must be called in the same order every render\n\
\n\
Help: Move this hook call to the top level of your component function.",
hook_name
),
);
self.errors.push(error);
}
}
pub fn validate(mut self, body: &mut syn::Block) -> Result<(), Vec<syn::Error>> {
self.visit_block_mut(body);
if self.errors.is_empty() {
Ok(())
} else {
Err(self.errors)
}
}
}
impl VisitMut for HookValidator {
fn visit_expr_call_mut(&mut self, node: &mut ExprCall) {
if let Some(path) = Self::is_hook_call(&node.func) {
self.validate_hook_call(path, node.span());
}
visit_mut::visit_expr_call_mut(self, node);
}
fn visit_expr_closure_mut(&mut self, node: &mut ExprClosure) {
self.with_branch(|validator| {
visit_mut::visit_expr_closure_mut(validator, node);
});
}
fn visit_expr_if_mut(&mut self, node: &mut ExprIf) {
for attr in &mut node.attrs {
visit_mut::visit_attribute_mut(self, attr);
}
visit_mut::visit_expr_mut(self, &mut node.cond);
self.with_branch(|validator| {
visit_mut::visit_block_mut(validator, &mut node.then_branch);
});
if let Some((_, else_expr)) = &mut node.else_branch {
self.with_branch(|validator| {
visit_mut::visit_expr_mut(validator, else_expr);
});
}
}
fn visit_expr_match_mut(&mut self, node: &mut ExprMatch) {
for attr in &mut node.attrs {
visit_mut::visit_attribute_mut(self, attr);
}
visit_mut::visit_expr_mut(self, &mut node.expr);
self.with_branch(|validator| {
for arm in &mut node.arms {
visit_mut::visit_arm_mut(validator, arm);
}
});
}
fn visit_expr_for_loop_mut(&mut self, node: &mut ExprForLoop) {
for attr in &mut node.attrs {
visit_mut::visit_attribute_mut(self, attr);
}
if let Some(label) = &mut node.label {
visit_mut::visit_label_mut(self, label);
}
visit_mut::visit_pat_mut(self, &mut node.pat);
visit_mut::visit_expr_mut(self, &mut node.expr);
self.with_branch(|validator| {
visit_mut::visit_block_mut(validator, &mut node.body);
});
}
fn visit_expr_while_mut(&mut self, node: &mut ExprWhile) {
for attr in &mut node.attrs {
visit_mut::visit_attribute_mut(self, attr);
}
if let Some(label) = &mut node.label {
visit_mut::visit_label_mut(self, label);
}
visit_mut::visit_expr_mut(self, &mut node.cond);
self.with_branch(|validator| {
visit_mut::visit_block_mut(validator, &mut node.body);
});
}
fn visit_expr_loop_mut(&mut self, node: &mut ExprLoop) {
self.with_branch(|validator| {
visit_mut::visit_expr_loop_mut(validator, node);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_valid_hook_at_top_level() {
let mut body: syn::Block = parse_quote! {
{
let state = use_state(|| 0);
let effect = use_effect(|| {}, ());
}
};
let validator = HookValidator::new();
let result = validator.validate(&mut body);
assert!(result.is_ok());
}
#[test]
fn test_invalid_hook_in_if() {
let mut body: syn::Block = parse_quote! {
{
if condition {
let state = use_state(|| 0);
}
}
};
let validator = HookValidator::new();
let result = validator.validate(&mut body);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].to_string().contains("cannot be called inside"));
}
#[test]
fn test_invalid_hook_in_loop() {
let mut body: syn::Block = parse_quote! {
{
for i in 0..10 {
let state = use_state(|| i);
}
}
};
let validator = HookValidator::new();
let result = validator.validate(&mut body);
assert!(result.is_err());
}
#[test]
fn test_invalid_hook_in_closure() {
let mut body: syn::Block = parse_quote! {
{
let callback = || {
let state = use_state(|| 0);
};
}
};
let validator = HookValidator::new();
let result = validator.validate(&mut body);
assert!(result.is_err());
}
#[test]
fn test_hook_in_effect_closure_is_invalid() {
let mut body: syn::Block = parse_quote! {
{
use_effect(|| {
let state = use_state(|| 0);
}, ());
}
};
let validator = HookValidator::new();
let result = validator.validate(&mut body);
assert!(result.is_err());
}
}