dioxus_check/
check.rs

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
27/// Checks a Dioxus file for issues.
28pub 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            // if the closure is a hook, we only visit the body of the closure.
201            // this prevents adding a ClosureInfo node to the context
202            syn::visit::visit_expr(self, body);
203        } else {
204            // otherwise visit the whole local
205            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        // only visit the body and else branch, calling hooks inside the expression is not conditional
219        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        // only visit the arms, calling hooks inside the expression is not conditional
236        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}