1use std::path::PathBuf;
2
3use syn::{spanned::Spanned, visit::Visit, Pat};
4
5use crate::{
6 issues::{Issue, IssueReport},
7 metadata::{
8 AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
9 HookInfo, IfInfo, LoopInfo, MatchInfo, Span, WhileInfo,
10 },
11};
12
13struct VisitHooks {
14 issues: Vec<Issue>,
15 context: Vec<Node>,
16}
17
18impl VisitHooks {
19 const fn new() -> Self {
20 Self {
21 issues: vec![],
22 context: vec![],
23 }
24 }
25}
26
27pub fn check_file(path: PathBuf, file_content: &str) -> IssueReport {
29 let file = syn::parse_file(file_content).unwrap();
30 let mut visit_hooks = VisitHooks::new();
31 visit_hooks.visit_file(&file);
32 IssueReport::new(
33 path,
34 std::env::current_dir().unwrap_or_default(),
35 file_content.to_string(),
36 visit_hooks.issues,
37 )
38}
39
40#[allow(unused)]
41#[derive(Debug, Clone)]
42enum Node {
43 If(IfInfo),
44 Match(MatchInfo),
45 For(ForInfo),
46 While(WhileInfo),
47 Loop(LoopInfo),
48 Closure(ClosureInfo),
49 Async(AsyncInfo),
50 ComponentFn(ComponentInfo),
51 HookFn(HookInfo),
52 OtherFn(FnInfo),
53}
54
55fn returns_element(ty: &syn::ReturnType) -> bool {
56 match ty {
57 syn::ReturnType::Default => false,
58 syn::ReturnType::Type(_, ref ty) => {
59 if let syn::Type::Path(ref path) = **ty {
60 if let Some(segment) = path.path.segments.last() {
61 if segment.ident == "Element" {
62 return true;
63 }
64 }
65 }
66 false
67 }
68 }
69}
70
71fn is_hook_ident(ident: &syn::Ident) -> bool {
72 ident.to_string().starts_with("use_")
73}
74
75fn is_component_fn(item_fn: &syn::ItemFn) -> bool {
76 returns_element(&item_fn.sig.output)
77}
78
79fn get_closure_hook_body(local: &syn::Local) -> Option<&syn::Expr> {
80 if let Pat::Ident(ident) = &local.pat {
81 if is_hook_ident(&ident.ident) {
82 if let Some(init) = &local.init {
83 if let syn::Expr::Closure(closure) = init.expr.as_ref() {
84 return Some(&closure.body);
85 }
86 }
87 }
88 }
89
90 None
91}
92
93fn fn_name_and_name_span(item_fn: &syn::ItemFn) -> (String, Span) {
94 let name = item_fn.sig.ident.to_string();
95 let name_span = item_fn.sig.ident.span().into();
96 (name, name_span)
97}
98
99impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
100 fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) {
101 if let syn::Expr::Path(ref path) = *i.func {
102 if let Some(segment) = path.path.segments.last() {
103 if is_hook_ident(&segment.ident) {
104 let hook_info = HookInfo::new(
105 i.span().into(),
106 segment.ident.span().into(),
107 segment.ident.to_string(),
108 );
109 let mut container_fn: Option<Node> = None;
110 for node in self.context.iter().rev() {
111 match &node {
112 Node::If(if_info) => {
113 let issue = Issue::HookInsideConditional(
114 hook_info.clone(),
115 ConditionalInfo::If(if_info.clone()),
116 );
117 self.issues.push(issue);
118 }
119 Node::Match(match_info) => {
120 let issue = Issue::HookInsideConditional(
121 hook_info.clone(),
122 ConditionalInfo::Match(match_info.clone()),
123 );
124 self.issues.push(issue);
125 }
126 Node::For(for_info) => {
127 let issue = Issue::HookInsideLoop(
128 hook_info.clone(),
129 AnyLoopInfo::For(for_info.clone()),
130 );
131 self.issues.push(issue);
132 }
133 Node::While(while_info) => {
134 let issue = Issue::HookInsideLoop(
135 hook_info.clone(),
136 AnyLoopInfo::While(while_info.clone()),
137 );
138 self.issues.push(issue);
139 }
140 Node::Loop(loop_info) => {
141 let issue = Issue::HookInsideLoop(
142 hook_info.clone(),
143 AnyLoopInfo::Loop(loop_info.clone()),
144 );
145 self.issues.push(issue);
146 }
147 Node::Closure(closure_info) => {
148 let issue = Issue::HookInsideClosure(
149 hook_info.clone(),
150 closure_info.clone(),
151 );
152 self.issues.push(issue);
153 }
154 Node::Async(async_info) => {
155 let issue =
156 Issue::HookInsideAsync(hook_info.clone(), async_info.clone());
157 self.issues.push(issue);
158 }
159 Node::ComponentFn(_) | Node::HookFn(_) | Node::OtherFn(_) => {
160 container_fn = Some(node.clone());
161 break;
162 }
163 }
164 }
165
166 if let Some(Node::OtherFn(_)) = container_fn {
167 let issue = Issue::HookOutsideComponent(hook_info);
168 self.issues.push(issue);
169 }
170 }
171 }
172 }
173 syn::visit::visit_expr_call(self, i);
174 }
175
176 fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
177 let (name, name_span) = fn_name_and_name_span(i);
178 if is_component_fn(i) {
179 self.context.push(Node::ComponentFn(ComponentInfo::new(
180 i.span().into(),
181 name,
182 name_span,
183 )));
184 } else if is_hook_ident(&i.sig.ident) {
185 self.context.push(Node::HookFn(HookInfo::new(
186 i.span().into(),
187 i.sig.ident.span().into(),
188 name,
189 )));
190 } else {
191 self.context
192 .push(Node::OtherFn(FnInfo::new(i.span().into(), name, name_span)));
193 }
194 syn::visit::visit_item_fn(self, i);
195 self.context.pop();
196 }
197
198 fn visit_local(&mut self, i: &'ast syn::Local) {
199 if let Some(body) = get_closure_hook_body(i) {
200 syn::visit::visit_expr(self, body);
203 } else {
204 syn::visit::visit_local(self, i);
206 }
207 }
208
209 fn visit_expr_if(&mut self, i: &'ast syn::ExprIf) {
210 self.context.push(Node::If(IfInfo::new(
211 i.span().into(),
212 i.if_token
213 .span()
214 .join(i.cond.span())
215 .unwrap_or_else(|| i.span())
216 .into(),
217 )));
218 self.visit_block(&i.then_branch);
220 if let Some(it) = &i.else_branch {
221 self.visit_expr(&(it).1);
222 }
223 self.context.pop();
224 }
225
226 fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
227 self.context.push(Node::Match(MatchInfo::new(
228 i.span().into(),
229 i.match_token
230 .span()
231 .join(i.expr.span())
232 .unwrap_or_else(|| i.span())
233 .into(),
234 )));
235 for it in &i.arms {
237 self.visit_arm(it);
238 }
239 self.context.pop();
240 }
241
242 fn visit_expr_for_loop(&mut self, i: &'ast syn::ExprForLoop) {
243 self.context.push(Node::For(ForInfo::new(
244 i.span().into(),
245 i.for_token
246 .span()
247 .join(i.expr.span())
248 .unwrap_or_else(|| i.span())
249 .into(),
250 )));
251 syn::visit::visit_expr_for_loop(self, i);
252 self.context.pop();
253 }
254
255 fn visit_expr_while(&mut self, i: &'ast syn::ExprWhile) {
256 self.context.push(Node::While(WhileInfo::new(
257 i.span().into(),
258 i.while_token
259 .span()
260 .join(i.cond.span())
261 .unwrap_or_else(|| i.span())
262 .into(),
263 )));
264 syn::visit::visit_expr_while(self, i);
265 self.context.pop();
266 }
267
268 fn visit_expr_loop(&mut self, i: &'ast syn::ExprLoop) {
269 self.context
270 .push(Node::Loop(LoopInfo::new(i.span().into())));
271 syn::visit::visit_expr_loop(self, i);
272 self.context.pop();
273 }
274
275 fn visit_expr_closure(&mut self, i: &'ast syn::ExprClosure) {
276 self.context
277 .push(Node::Closure(ClosureInfo::new(i.span().into())));
278 syn::visit::visit_expr_closure(self, i);
279 self.context.pop();
280 }
281
282 fn visit_expr_async(&mut self, i: &'ast syn::ExprAsync) {
283 self.context
284 .push(Node::Async(AsyncInfo::new(i.span().into())));
285 syn::visit::visit_expr_async(self, i);
286 self.context.pop();
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use crate::metadata::{
293 AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
294 MatchInfo, Span, WhileInfo,
295 };
296 use indoc::indoc;
297 use pretty_assertions::assert_eq;
298
299 use super::*;
300
301 #[test]
302 fn test_no_hooks() {
303 let contents = indoc! {r#"
304 fn App() -> Element {
305 rsx! {
306 p { "Hello World" }
307 }
308 }
309 "#};
310
311 let report = check_file("app.rs".into(), contents);
312
313 assert_eq!(report.issues, vec![]);
314 }
315
316 #[test]
317 fn test_hook_correctly_used_inside_component() {
318 let contents = indoc! {r#"
319 fn App() -> Element {
320 let count = use_signal(|| 0);
321 rsx! {
322 p { "Hello World: {count}" }
323 }
324 }
325 "#};
326
327 let report = check_file("app.rs".into(), contents);
328
329 assert_eq!(report.issues, vec![]);
330 }
331
332 #[test]
333 fn test_hook_correctly_used_inside_hook_fn() {
334 let contents = indoc! {r#"
335 fn use_thing() -> UseState<i32> {
336 use_signal(|| 0)
337 }
338 "#};
339
340 let report = check_file("use_thing.rs".into(), contents);
341
342 assert_eq!(report.issues, vec![]);
343 }
344
345 #[test]
346 fn test_hook_correctly_used_inside_hook_closure() {
347 let contents = indoc! {r#"
348 fn App() -> Element {
349 let use_thing = || {
350 use_signal(|| 0)
351 };
352 let count = use_thing();
353 rsx! {
354 p { "Hello World: {count}" }
355 }
356 }
357 "#};
358
359 let report = check_file("app.rs".into(), contents);
360
361 assert_eq!(report.issues, vec![]);
362 }
363
364 #[test]
365 fn test_conditional_hook_if() {
366 let contents = indoc! {r#"
367 fn App() -> Element {
368 if you_are_happy && you_know_it {
369 let something = use_signal(|| "hands");
370 println!("clap your {something}")
371 }
372 }
373 "#};
374
375 let report = check_file("app.rs".into(), contents);
376
377 assert_eq!(
378 report.issues,
379 vec![Issue::HookInsideConditional(
380 HookInfo::new(
381 Span::new_from_str(
382 r#"use_signal(|| "hands")"#,
383 LineColumn { line: 3, column: 24 },
384 ),
385 Span::new_from_str(
386 r#"use_signal"#,
387 LineColumn { line: 3, column: 24 },
388 ),
389 "use_signal".to_string()
390 ),
391 ConditionalInfo::If(IfInfo::new(
392 Span::new_from_str(
393 "if you_are_happy && you_know_it {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
394 LineColumn { line: 2, column: 4 },
395 ),
396 Span::new_from_str(
397 "if you_are_happy && you_know_it",
398 LineColumn { line: 2, column: 4 }
399 )
400 ))
401 )],
402 );
403 }
404
405 #[test]
406 fn test_conditional_hook_match() {
407 let contents = indoc! {r#"
408 fn App() -> Element {
409 match you_are_happy && you_know_it {
410 true => {
411 let something = use_signal(|| "hands");
412 println!("clap your {something}")
413 }
414 false => {}
415 }
416 }
417 "#};
418
419 let report = check_file("app.rs".into(), contents);
420
421 assert_eq!(
422 report.issues,
423 vec![Issue::HookInsideConditional(
424 HookInfo::new(
425 Span::new_from_str(r#"use_signal(|| "hands")"#, LineColumn { line: 4, column: 28 }),
426 Span::new_from_str(r#"use_signal"#, LineColumn { line: 4, column: 28 }),
427 "use_signal".to_string()
428 ),
429 ConditionalInfo::Match(MatchInfo::new(
430 Span::new_from_str(
431 "match you_are_happy && you_know_it {\n true => {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }\n false => {}\n }",
432 LineColumn { line: 2, column: 4 },
433 ),
434 Span::new_from_str("match you_are_happy && you_know_it", LineColumn { line: 2, column: 4 })
435 ))
436 )]
437 );
438 }
439
440 #[test]
441 fn test_use_in_match_expr() {
442 let contents = indoc! {r#"
443 fn use_thing() {
444 match use_resource(|| async {}) {
445 Ok(_) => {}
446 Err(_) => {}
447 }
448 }
449 "#};
450
451 let report = check_file("app.rs".into(), contents);
452
453 assert_eq!(report.issues, vec![]);
454 }
455
456 #[test]
457 fn test_for_loop_hook() {
458 let contents = indoc! {r#"
459 fn App() -> Element {
460 for _name in &names {
461 let is_selected = use_signal(|| false);
462 println!("selected: {is_selected}");
463 }
464 }
465 "#};
466
467 let report = check_file("app.rs".into(), contents);
468
469 assert_eq!(
470 report.issues,
471 vec![Issue::HookInsideLoop(
472 HookInfo::new(
473 Span::new_from_str(
474 "use_signal(|| false)",
475 LineColumn { line: 3, column: 26 },
476 ),
477 Span::new_from_str(
478 "use_signal",
479 LineColumn { line: 3, column: 26 },
480 ),
481 "use_signal".to_string()
482 ),
483 AnyLoopInfo::For(ForInfo::new(
484 Span::new_from_str(
485 "for _name in &names {\n let is_selected = use_signal(|| false);\n println!(\"selected: {is_selected}\");\n }",
486 LineColumn { line: 2, column: 4 },
487 ),
488 Span::new_from_str(
489 "for _name in &names",
490 LineColumn { line: 2, column: 4 },
491 )
492 ))
493 )]
494 );
495 }
496
497 #[test]
498 fn test_while_loop_hook() {
499 let contents = indoc! {r#"
500 fn App() -> Element {
501 while true {
502 let something = use_signal(|| "hands");
503 println!("clap your {something}")
504 }
505 }
506 "#};
507
508 let report = check_file("app.rs".into(), contents);
509
510 assert_eq!(
511 report.issues,
512 vec![Issue::HookInsideLoop(
513 HookInfo::new(
514 Span::new_from_str(
515 r#"use_signal(|| "hands")"#,
516 LineColumn { line: 3, column: 24 },
517 ),
518 Span::new_from_str(
519 "use_signal",
520 LineColumn { line: 3, column: 24 },
521 ),
522 "use_signal".to_string()
523 ),
524 AnyLoopInfo::While(WhileInfo::new(
525 Span::new_from_str(
526 "while true {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
527 LineColumn { line: 2, column: 4 },
528 ),
529 Span::new_from_str(
530 "while true",
531 LineColumn { line: 2, column: 4 },
532 )
533 ))
534 )],
535 );
536 }
537
538 #[test]
539 fn test_loop_hook() {
540 let contents = indoc! {r#"
541 fn App() -> Element {
542 loop {
543 let something = use_signal(|| "hands");
544 println!("clap your {something}")
545 }
546 }
547 "#};
548
549 let report = check_file("app.rs".into(), contents);
550
551 assert_eq!(
552 report.issues,
553 vec![Issue::HookInsideLoop(
554 HookInfo::new(
555 Span::new_from_str(
556 r#"use_signal(|| "hands")"#,
557 LineColumn { line: 3, column: 24 },
558 ),
559 Span::new_from_str(
560 "use_signal",
561 LineColumn { line: 3, column: 24 },
562 ),
563 "use_signal".to_string()
564 ),
565 AnyLoopInfo::Loop(LoopInfo::new(Span::new_from_str(
566 "loop {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
567 LineColumn { line: 2, column: 4 },
568 )))
569 )],
570 );
571 }
572
573 #[test]
574 fn test_conditional_okay() {
575 let contents = indoc! {r#"
576 fn App() -> Element {
577 let something = use_signal(|| "hands");
578 if you_are_happy && you_know_it {
579 println!("clap your {something}")
580 }
581 }
582 "#};
583
584 let report = check_file("app.rs".into(), contents);
585
586 assert_eq!(report.issues, vec![]);
587 }
588
589 #[test]
590 fn test_conditional_expr_okay() {
591 let contents = indoc! {r#"
592 fn App() -> Element {
593 if use_signal(|| true) {
594 println!("clap your {something}")
595 }
596 }
597 "#};
598
599 let report = check_file("app.rs".into(), contents);
600
601 assert_eq!(report.issues, vec![]);
602 }
603
604 #[test]
605 fn test_closure_hook() {
606 let contents = indoc! {r#"
607 fn App() -> Element {
608 let _a = || {
609 let b = use_signal(|| 0);
610 b.get()
611 };
612 }
613 "#};
614
615 let report = check_file("app.rs".into(), contents);
616
617 assert_eq!(
618 report.issues,
619 vec![Issue::HookInsideClosure(
620 HookInfo::new(
621 Span::new_from_str(
622 "use_signal(|| 0)",
623 LineColumn {
624 line: 3,
625 column: 16
626 },
627 ),
628 Span::new_from_str(
629 "use_signal",
630 LineColumn {
631 line: 3,
632 column: 16
633 },
634 ),
635 "use_signal".to_string()
636 ),
637 ClosureInfo::new(Span::new_from_str(
638 "|| {\n let b = use_signal(|| 0);\n b.get()\n }",
639 LineColumn {
640 line: 2,
641 column: 13
642 },
643 ))
644 )]
645 );
646 }
647
648 #[test]
649 fn test_hook_outside_component() {
650 let contents = indoc! {r#"
651 fn not_component_or_hook() {
652 let _a = use_signal(|| 0);
653 }
654 "#};
655
656 let report = check_file("app.rs".into(), contents);
657
658 assert_eq!(
659 report.issues,
660 vec![Issue::HookOutsideComponent(HookInfo::new(
661 Span::new_from_str(
662 "use_signal(|| 0)",
663 LineColumn {
664 line: 2,
665 column: 13
666 }
667 ),
668 Span::new_from_str(
669 "use_signal",
670 LineColumn {
671 line: 2,
672 column: 13
673 },
674 ),
675 "use_signal".to_string()
676 ))]
677 );
678 }
679
680 #[test]
681 fn test_hook_inside_hook() {
682 let contents = indoc! {r#"
683 fn use_thing() {
684 let _a = use_signal(|| 0);
685 }
686 "#};
687
688 let report = check_file("app.rs".into(), contents);
689
690 assert_eq!(report.issues, vec![]);
691 }
692
693 #[test]
694 fn test_hook_inside_hook_initialization() {
695 let contents = indoc! {r#"
696 fn use_thing() {
697 let _a = use_signal(|| use_signal(|| 0));
698 }
699 "#};
700
701 let report = check_file("app.rs".into(), contents);
702
703 assert_eq!(
704 report.issues,
705 vec![Issue::HookInsideClosure(
706 HookInfo::new(
707 Span::new_from_str(
708 "use_signal(|| 0)",
709 LineColumn {
710 line: 2,
711 column: 27,
712 },
713 ),
714 Span::new_from_str(
715 "use_signal",
716 LineColumn {
717 line: 2,
718 column: 27,
719 },
720 ),
721 "use_signal".to_string()
722 ),
723 ClosureInfo::new(Span::new_from_str(
724 "|| use_signal(|| 0)",
725 LineColumn {
726 line: 2,
727 column: 24,
728 },
729 ))
730 ),]
731 );
732 }
733
734 #[test]
735 fn test_hook_inside_hook_async_initialization() {
736 let contents = indoc! {r#"
737 fn use_thing() {
738 let _a = use_future(|| async move { use_signal(|| 0) });
739 }
740 "#};
741
742 let report = check_file("app.rs".into(), contents);
743
744 assert_eq!(
745 report.issues,
746 vec![
747 Issue::HookInsideAsync(
748 HookInfo::new(
749 Span::new_from_str(
750 "use_signal(|| 0)",
751 LineColumn {
752 line: 2,
753 column: 40,
754 },
755 ),
756 Span::new_from_str(
757 "use_signal",
758 LineColumn {
759 line: 2,
760 column: 40,
761 },
762 ),
763 "use_signal".to_string()
764 ),
765 AsyncInfo::new(Span::new_from_str(
766 "async move { use_signal(|| 0) }",
767 LineColumn {
768 line: 2,
769 column: 27,
770 },
771 ))
772 ),
773 Issue::HookInsideClosure(
774 HookInfo::new(
775 Span::new_from_str(
776 "use_signal(|| 0)",
777 LineColumn {
778 line: 2,
779 column: 40,
780 },
781 ),
782 Span::new_from_str(
783 "use_signal",
784 LineColumn {
785 line: 2,
786 column: 40,
787 },
788 ),
789 "use_signal".to_string()
790 ),
791 ClosureInfo::new(Span::new_from_str(
792 "|| async move { use_signal(|| 0) }",
793 LineColumn {
794 line: 2,
795 column: 24,
796 },
797 ))
798 ),
799 ]
800 );
801 }
802
803 #[test]
804 fn test_hook_inside_spawn() {
805 let contents = indoc! {r#"
806 fn use_thing() {
807 let _a = spawn(async move { use_signal(|| 0) });
808 }
809 "#};
810
811 let report = check_file("app.rs".into(), contents);
812
813 assert_eq!(
814 report.issues,
815 vec![Issue::HookInsideAsync(
816 HookInfo::new(
817 Span::new_from_str(
818 "use_signal(|| 0)",
819 LineColumn {
820 line: 2,
821 column: 32,
822 },
823 ),
824 Span::new_from_str(
825 "use_signal",
826 LineColumn {
827 line: 2,
828 column: 32,
829 },
830 ),
831 "use_signal".to_string()
832 ),
833 AsyncInfo::new(Span::new_from_str(
834 "async move { use_signal(|| 0) }",
835 LineColumn {
836 line: 2,
837 column: 19,
838 },
839 ))
840 ),]
841 );
842 }
843}