123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843 |
- use std::path::PathBuf;
- use syn::{spanned::Spanned, visit::Visit, Pat};
- use crate::{
- issues::{Issue, IssueReport},
- metadata::{
- AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
- HookInfo, IfInfo, LoopInfo, MatchInfo, Span, WhileInfo,
- },
- };
- struct VisitHooks {
- issues: Vec<Issue>,
- context: Vec<Node>,
- }
- impl VisitHooks {
- const fn new() -> Self {
- Self {
- issues: vec![],
- context: vec![],
- }
- }
- }
- /// Checks a Dioxus file for issues.
- pub fn check_file(path: PathBuf, file_content: &str) -> IssueReport {
- let file = syn::parse_file(file_content).unwrap();
- let mut visit_hooks = VisitHooks::new();
- visit_hooks.visit_file(&file);
- IssueReport::new(
- path,
- std::env::current_dir().unwrap_or_default(),
- file_content.to_string(),
- visit_hooks.issues,
- )
- }
- #[allow(unused)]
- #[derive(Debug, Clone)]
- enum Node {
- If(IfInfo),
- Match(MatchInfo),
- For(ForInfo),
- While(WhileInfo),
- Loop(LoopInfo),
- Closure(ClosureInfo),
- Async(AsyncInfo),
- ComponentFn(ComponentInfo),
- HookFn(HookInfo),
- OtherFn(FnInfo),
- }
- fn returns_element(ty: &syn::ReturnType) -> bool {
- match ty {
- syn::ReturnType::Default => false,
- syn::ReturnType::Type(_, ref ty) => {
- if let syn::Type::Path(ref path) = **ty {
- if let Some(segment) = path.path.segments.last() {
- if segment.ident == "Element" {
- return true;
- }
- }
- }
- false
- }
- }
- }
- fn is_hook_ident(ident: &syn::Ident) -> bool {
- ident.to_string().starts_with("use_")
- }
- fn is_component_fn(item_fn: &syn::ItemFn) -> bool {
- returns_element(&item_fn.sig.output)
- }
- fn get_closure_hook_body(local: &syn::Local) -> Option<&syn::Expr> {
- if let Pat::Ident(ident) = &local.pat {
- if is_hook_ident(&ident.ident) {
- if let Some(init) = &local.init {
- if let syn::Expr::Closure(closure) = init.expr.as_ref() {
- return Some(&closure.body);
- }
- }
- }
- }
- None
- }
- fn fn_name_and_name_span(item_fn: &syn::ItemFn) -> (String, Span) {
- let name = item_fn.sig.ident.to_string();
- let name_span = item_fn.sig.ident.span().into();
- (name, name_span)
- }
- impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
- fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) {
- if let syn::Expr::Path(ref path) = *i.func {
- if let Some(segment) = path.path.segments.last() {
- if is_hook_ident(&segment.ident) {
- let hook_info = HookInfo::new(
- i.span().into(),
- segment.ident.span().into(),
- segment.ident.to_string(),
- );
- let mut container_fn: Option<Node> = None;
- for node in self.context.iter().rev() {
- match &node {
- Node::If(if_info) => {
- let issue = Issue::HookInsideConditional(
- hook_info.clone(),
- ConditionalInfo::If(if_info.clone()),
- );
- self.issues.push(issue);
- }
- Node::Match(match_info) => {
- let issue = Issue::HookInsideConditional(
- hook_info.clone(),
- ConditionalInfo::Match(match_info.clone()),
- );
- self.issues.push(issue);
- }
- Node::For(for_info) => {
- let issue = Issue::HookInsideLoop(
- hook_info.clone(),
- AnyLoopInfo::For(for_info.clone()),
- );
- self.issues.push(issue);
- }
- Node::While(while_info) => {
- let issue = Issue::HookInsideLoop(
- hook_info.clone(),
- AnyLoopInfo::While(while_info.clone()),
- );
- self.issues.push(issue);
- }
- Node::Loop(loop_info) => {
- let issue = Issue::HookInsideLoop(
- hook_info.clone(),
- AnyLoopInfo::Loop(loop_info.clone()),
- );
- self.issues.push(issue);
- }
- Node::Closure(closure_info) => {
- let issue = Issue::HookInsideClosure(
- hook_info.clone(),
- closure_info.clone(),
- );
- self.issues.push(issue);
- }
- Node::Async(async_info) => {
- let issue =
- Issue::HookInsideAsync(hook_info.clone(), async_info.clone());
- self.issues.push(issue);
- }
- Node::ComponentFn(_) | Node::HookFn(_) | Node::OtherFn(_) => {
- container_fn = Some(node.clone());
- break;
- }
- }
- }
- if let Some(Node::OtherFn(_)) = container_fn {
- let issue = Issue::HookOutsideComponent(hook_info);
- self.issues.push(issue);
- }
- }
- }
- }
- syn::visit::visit_expr_call(self, i);
- }
- fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
- let (name, name_span) = fn_name_and_name_span(i);
- if is_component_fn(i) {
- self.context.push(Node::ComponentFn(ComponentInfo::new(
- i.span().into(),
- name,
- name_span,
- )));
- } else if is_hook_ident(&i.sig.ident) {
- self.context.push(Node::HookFn(HookInfo::new(
- i.span().into(),
- i.sig.ident.span().into(),
- name,
- )));
- } else {
- self.context
- .push(Node::OtherFn(FnInfo::new(i.span().into(), name, name_span)));
- }
- syn::visit::visit_item_fn(self, i);
- self.context.pop();
- }
- fn visit_local(&mut self, i: &'ast syn::Local) {
- if let Some(body) = get_closure_hook_body(i) {
- // if the closure is a hook, we only visit the body of the closure.
- // this prevents adding a ClosureInfo node to the context
- syn::visit::visit_expr(self, body);
- } else {
- // otherwise visit the whole local
- syn::visit::visit_local(self, i);
- }
- }
- fn visit_expr_if(&mut self, i: &'ast syn::ExprIf) {
- self.context.push(Node::If(IfInfo::new(
- i.span().into(),
- i.if_token
- .span()
- .join(i.cond.span())
- .unwrap_or_else(|| i.span())
- .into(),
- )));
- // only visit the body and else branch, calling hooks inside the expression is not conditional
- self.visit_block(&i.then_branch);
- if let Some(it) = &i.else_branch {
- self.visit_expr(&(it).1);
- }
- self.context.pop();
- }
- fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
- self.context.push(Node::Match(MatchInfo::new(
- i.span().into(),
- i.match_token
- .span()
- .join(i.expr.span())
- .unwrap_or_else(|| i.span())
- .into(),
- )));
- // only visit the arms, calling hooks inside the expression is not conditional
- for it in &i.arms {
- self.visit_arm(it);
- }
- self.context.pop();
- }
- fn visit_expr_for_loop(&mut self, i: &'ast syn::ExprForLoop) {
- self.context.push(Node::For(ForInfo::new(
- i.span().into(),
- i.for_token
- .span()
- .join(i.expr.span())
- .unwrap_or_else(|| i.span())
- .into(),
- )));
- syn::visit::visit_expr_for_loop(self, i);
- self.context.pop();
- }
- fn visit_expr_while(&mut self, i: &'ast syn::ExprWhile) {
- self.context.push(Node::While(WhileInfo::new(
- i.span().into(),
- i.while_token
- .span()
- .join(i.cond.span())
- .unwrap_or_else(|| i.span())
- .into(),
- )));
- syn::visit::visit_expr_while(self, i);
- self.context.pop();
- }
- fn visit_expr_loop(&mut self, i: &'ast syn::ExprLoop) {
- self.context
- .push(Node::Loop(LoopInfo::new(i.span().into())));
- syn::visit::visit_expr_loop(self, i);
- self.context.pop();
- }
- fn visit_expr_closure(&mut self, i: &'ast syn::ExprClosure) {
- self.context
- .push(Node::Closure(ClosureInfo::new(i.span().into())));
- syn::visit::visit_expr_closure(self, i);
- self.context.pop();
- }
- fn visit_expr_async(&mut self, i: &'ast syn::ExprAsync) {
- self.context
- .push(Node::Async(AsyncInfo::new(i.span().into())));
- syn::visit::visit_expr_async(self, i);
- self.context.pop();
- }
- }
- #[cfg(test)]
- mod tests {
- use crate::metadata::{
- AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
- MatchInfo, Span, WhileInfo,
- };
- use indoc::indoc;
- use pretty_assertions::assert_eq;
- use super::*;
- #[test]
- fn test_no_hooks() {
- let contents = indoc! {r#"
- fn App() -> Element {
- rsx! {
- p { "Hello World" }
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_hook_correctly_used_inside_component() {
- let contents = indoc! {r#"
- fn App() -> Element {
- let count = use_signal(|| 0);
- rsx! {
- p { "Hello World: {count}" }
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_hook_correctly_used_inside_hook_fn() {
- let contents = indoc! {r#"
- fn use_thing() -> UseState<i32> {
- use_signal(|| 0)
- }
- "#};
- let report = check_file("use_thing.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_hook_correctly_used_inside_hook_closure() {
- let contents = indoc! {r#"
- fn App() -> Element {
- let use_thing = || {
- use_signal(|| 0)
- };
- let count = use_thing();
- rsx! {
- p { "Hello World: {count}" }
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_conditional_hook_if() {
- let contents = indoc! {r#"
- fn App() -> Element {
- if you_are_happy && you_know_it {
- let something = use_signal(|| "hands");
- println!("clap your {something}")
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideConditional(
- HookInfo::new(
- Span::new_from_str(
- r#"use_signal(|| "hands")"#,
- LineColumn { line: 3, column: 24 },
- ),
- Span::new_from_str(
- r#"use_signal"#,
- LineColumn { line: 3, column: 24 },
- ),
- "use_signal".to_string()
- ),
- ConditionalInfo::If(IfInfo::new(
- Span::new_from_str(
- "if you_are_happy && you_know_it {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
- LineColumn { line: 2, column: 4 },
- ),
- Span::new_from_str(
- "if you_are_happy && you_know_it",
- LineColumn { line: 2, column: 4 }
- )
- ))
- )],
- );
- }
- #[test]
- fn test_conditional_hook_match() {
- let contents = indoc! {r#"
- fn App() -> Element {
- match you_are_happy && you_know_it {
- true => {
- let something = use_signal(|| "hands");
- println!("clap your {something}")
- }
- false => {}
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideConditional(
- HookInfo::new(
- Span::new_from_str(r#"use_signal(|| "hands")"#, LineColumn { line: 4, column: 28 }),
- Span::new_from_str(r#"use_signal"#, LineColumn { line: 4, column: 28 }),
- "use_signal".to_string()
- ),
- ConditionalInfo::Match(MatchInfo::new(
- Span::new_from_str(
- "match you_are_happy && you_know_it {\n true => {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }\n false => {}\n }",
- LineColumn { line: 2, column: 4 },
- ),
- Span::new_from_str("match you_are_happy && you_know_it", LineColumn { line: 2, column: 4 })
- ))
- )]
- );
- }
- #[test]
- fn test_use_in_match_expr() {
- let contents = indoc! {r#"
- fn use_thing() {
- match use_resource(|| async {}) {
- Ok(_) => {}
- Err(_) => {}
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_for_loop_hook() {
- let contents = indoc! {r#"
- fn App() -> Element {
- for _name in &names {
- let is_selected = use_signal(|| false);
- println!("selected: {is_selected}");
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideLoop(
- HookInfo::new(
- Span::new_from_str(
- "use_signal(|| false)",
- LineColumn { line: 3, column: 26 },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn { line: 3, column: 26 },
- ),
- "use_signal".to_string()
- ),
- AnyLoopInfo::For(ForInfo::new(
- Span::new_from_str(
- "for _name in &names {\n let is_selected = use_signal(|| false);\n println!(\"selected: {is_selected}\");\n }",
- LineColumn { line: 2, column: 4 },
- ),
- Span::new_from_str(
- "for _name in &names",
- LineColumn { line: 2, column: 4 },
- )
- ))
- )]
- );
- }
- #[test]
- fn test_while_loop_hook() {
- let contents = indoc! {r#"
- fn App() -> Element {
- while true {
- let something = use_signal(|| "hands");
- println!("clap your {something}")
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideLoop(
- HookInfo::new(
- Span::new_from_str(
- r#"use_signal(|| "hands")"#,
- LineColumn { line: 3, column: 24 },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn { line: 3, column: 24 },
- ),
- "use_signal".to_string()
- ),
- AnyLoopInfo::While(WhileInfo::new(
- Span::new_from_str(
- "while true {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
- LineColumn { line: 2, column: 4 },
- ),
- Span::new_from_str(
- "while true",
- LineColumn { line: 2, column: 4 },
- )
- ))
- )],
- );
- }
- #[test]
- fn test_loop_hook() {
- let contents = indoc! {r#"
- fn App() -> Element {
- loop {
- let something = use_signal(|| "hands");
- println!("clap your {something}")
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideLoop(
- HookInfo::new(
- Span::new_from_str(
- r#"use_signal(|| "hands")"#,
- LineColumn { line: 3, column: 24 },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn { line: 3, column: 24 },
- ),
- "use_signal".to_string()
- ),
- AnyLoopInfo::Loop(LoopInfo::new(Span::new_from_str(
- "loop {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
- LineColumn { line: 2, column: 4 },
- )))
- )],
- );
- }
- #[test]
- fn test_conditional_okay() {
- let contents = indoc! {r#"
- fn App() -> Element {
- let something = use_signal(|| "hands");
- if you_are_happy && you_know_it {
- println!("clap your {something}")
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_conditional_expr_okay() {
- let contents = indoc! {r#"
- fn App() -> Element {
- if use_signal(|| true) {
- println!("clap your {something}")
- }
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_closure_hook() {
- let contents = indoc! {r#"
- fn App() -> Element {
- let _a = || {
- let b = use_signal(|| 0);
- b.get()
- };
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideClosure(
- HookInfo::new(
- Span::new_from_str(
- "use_signal(|| 0)",
- LineColumn {
- line: 3,
- column: 16
- },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn {
- line: 3,
- column: 16
- },
- ),
- "use_signal".to_string()
- ),
- ClosureInfo::new(Span::new_from_str(
- "|| {\n let b = use_signal(|| 0);\n b.get()\n }",
- LineColumn {
- line: 2,
- column: 13
- },
- ))
- )]
- );
- }
- #[test]
- fn test_hook_outside_component() {
- let contents = indoc! {r#"
- fn not_component_or_hook() {
- let _a = use_signal(|| 0);
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookOutsideComponent(HookInfo::new(
- Span::new_from_str(
- "use_signal(|| 0)",
- LineColumn {
- line: 2,
- column: 13
- }
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn {
- line: 2,
- column: 13
- },
- ),
- "use_signal".to_string()
- ))]
- );
- }
- #[test]
- fn test_hook_inside_hook() {
- let contents = indoc! {r#"
- fn use_thing() {
- let _a = use_signal(|| 0);
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(report.issues, vec![]);
- }
- #[test]
- fn test_hook_inside_hook_initialization() {
- let contents = indoc! {r#"
- fn use_thing() {
- let _a = use_signal(|| use_signal(|| 0));
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideClosure(
- HookInfo::new(
- Span::new_from_str(
- "use_signal(|| 0)",
- LineColumn {
- line: 2,
- column: 27,
- },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn {
- line: 2,
- column: 27,
- },
- ),
- "use_signal".to_string()
- ),
- ClosureInfo::new(Span::new_from_str(
- "|| use_signal(|| 0)",
- LineColumn {
- line: 2,
- column: 24,
- },
- ))
- ),]
- );
- }
- #[test]
- fn test_hook_inside_hook_async_initialization() {
- let contents = indoc! {r#"
- fn use_thing() {
- let _a = use_future(|| async move { use_signal(|| 0) });
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![
- Issue::HookInsideAsync(
- HookInfo::new(
- Span::new_from_str(
- "use_signal(|| 0)",
- LineColumn {
- line: 2,
- column: 40,
- },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn {
- line: 2,
- column: 40,
- },
- ),
- "use_signal".to_string()
- ),
- AsyncInfo::new(Span::new_from_str(
- "async move { use_signal(|| 0) }",
- LineColumn {
- line: 2,
- column: 27,
- },
- ))
- ),
- Issue::HookInsideClosure(
- HookInfo::new(
- Span::new_from_str(
- "use_signal(|| 0)",
- LineColumn {
- line: 2,
- column: 40,
- },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn {
- line: 2,
- column: 40,
- },
- ),
- "use_signal".to_string()
- ),
- ClosureInfo::new(Span::new_from_str(
- "|| async move { use_signal(|| 0) }",
- LineColumn {
- line: 2,
- column: 24,
- },
- ))
- ),
- ]
- );
- }
- #[test]
- fn test_hook_inside_spawn() {
- let contents = indoc! {r#"
- fn use_thing() {
- let _a = spawn(async move { use_signal(|| 0) });
- }
- "#};
- let report = check_file("app.rs".into(), contents);
- assert_eq!(
- report.issues,
- vec![Issue::HookInsideAsync(
- HookInfo::new(
- Span::new_from_str(
- "use_signal(|| 0)",
- LineColumn {
- line: 2,
- column: 32,
- },
- ),
- Span::new_from_str(
- "use_signal",
- LineColumn {
- line: 2,
- column: 32,
- },
- ),
- "use_signal".to_string()
- ),
- AsyncInfo::new(Span::new_from_str(
- "async move { use_signal(|| 0) }",
- LineColumn {
- line: 2,
- column: 19,
- },
- ))
- ),]
- );
- }
- }
|