mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Allow hooks in the expression of a match statement and if statement (#2902)
* Allow hooks in the expression of a match statement and if statement * Don't allow hooks in spawn and detect hooks in initialization closures * Point to DX check when hooks are called conditionally
This commit is contained in:
parent
7bb53fe835
commit
c11f2ed3cd
4 changed files with 232 additions and 7 deletions
|
@ -5,8 +5,8 @@ use syn::{spanned::Spanned, visit::Visit, Pat};
|
||||||
use crate::{
|
use crate::{
|
||||||
issues::{Issue, IssueReport},
|
issues::{Issue, IssueReport},
|
||||||
metadata::{
|
metadata::{
|
||||||
AnyLoopInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo, HookInfo,
|
AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
|
||||||
IfInfo, LoopInfo, MatchInfo, Span, WhileInfo,
|
HookInfo, IfInfo, LoopInfo, MatchInfo, Span, WhileInfo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ enum Node {
|
||||||
While(WhileInfo),
|
While(WhileInfo),
|
||||||
Loop(LoopInfo),
|
Loop(LoopInfo),
|
||||||
Closure(ClosureInfo),
|
Closure(ClosureInfo),
|
||||||
|
Async(AsyncInfo),
|
||||||
ComponentFn(ComponentInfo),
|
ComponentFn(ComponentInfo),
|
||||||
HookFn(HookInfo),
|
HookFn(HookInfo),
|
||||||
OtherFn(FnInfo),
|
OtherFn(FnInfo),
|
||||||
|
@ -107,7 +108,7 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||||
);
|
);
|
||||||
let mut container_fn: Option<Node> = None;
|
let mut container_fn: Option<Node> = None;
|
||||||
for node in self.context.iter().rev() {
|
for node in self.context.iter().rev() {
|
||||||
match node {
|
match &node {
|
||||||
Node::If(if_info) => {
|
Node::If(if_info) => {
|
||||||
let issue = Issue::HookInsideConditional(
|
let issue = Issue::HookInsideConditional(
|
||||||
hook_info.clone(),
|
hook_info.clone(),
|
||||||
|
@ -150,6 +151,11 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||||
);
|
);
|
||||||
self.issues.push(issue);
|
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(_) => {
|
Node::ComponentFn(_) | Node::HookFn(_) | Node::OtherFn(_) => {
|
||||||
container_fn = Some(node.clone());
|
container_fn = Some(node.clone());
|
||||||
break;
|
break;
|
||||||
|
@ -164,6 +170,7 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
syn::visit::visit_expr_call(self, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
|
fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
|
||||||
|
@ -208,7 +215,11 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||||
.unwrap_or_else(|| i.span())
|
.unwrap_or_else(|| i.span())
|
||||||
.into(),
|
.into(),
|
||||||
)));
|
)));
|
||||||
syn::visit::visit_expr_if(self, i);
|
// 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();
|
self.context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +232,10 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||||
.unwrap_or_else(|| i.span())
|
.unwrap_or_else(|| i.span())
|
||||||
.into(),
|
.into(),
|
||||||
)));
|
)));
|
||||||
syn::visit::visit_expr_match(self, i);
|
// only visit the arms, calling hooks inside the expression is not conditional
|
||||||
|
for it in &i.arms {
|
||||||
|
self.visit_arm(it);
|
||||||
|
}
|
||||||
self.context.pop();
|
self.context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +278,13 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||||
syn::visit::visit_expr_closure(self, i);
|
syn::visit::visit_expr_closure(self, i);
|
||||||
self.context.pop();
|
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)]
|
#[cfg(test)]
|
||||||
|
@ -416,6 +437,22 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn test_for_loop_hook() {
|
fn test_for_loop_hook() {
|
||||||
let contents = indoc! {r#"
|
let contents = indoc! {r#"
|
||||||
|
@ -549,6 +586,21 @@ mod tests {
|
||||||
assert_eq!(report.issues, vec![]);
|
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]
|
#[test]
|
||||||
fn test_closure_hook() {
|
fn test_closure_hook() {
|
||||||
let contents = indoc! {r#"
|
let contents = indoc! {r#"
|
||||||
|
@ -637,4 +689,155 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(report.issues, vec![]);
|
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,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
),]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::metadata::{
|
use crate::metadata::{
|
||||||
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo, WhileInfo,
|
AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo,
|
||||||
|
WhileInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The result of checking a Dioxus file for issues.
|
/// The result of checking a Dioxus file for issues.
|
||||||
|
@ -142,7 +143,9 @@ impl Display for IssueReport {
|
||||||
Issue::HookInsideLoop(_, AnyLoopInfo::Loop(_)) => {
|
Issue::HookInsideLoop(_, AnyLoopInfo::Loop(_)) => {
|
||||||
writeln!(f, "{} `loop {{ … }}` is the loop", note_text_prefix,)?;
|
writeln!(f, "{} `loop {{ … }}` is the loop", note_text_prefix,)?;
|
||||||
}
|
}
|
||||||
Issue::HookOutsideComponent(_) | Issue::HookInsideClosure(_, _) => {}
|
Issue::HookOutsideComponent(_)
|
||||||
|
| Issue::HookInsideClosure(_, _)
|
||||||
|
| Issue::HookInsideAsync(_, _) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if i < self.issues.len() - 1 {
|
if i < self.issues.len() - 1 {
|
||||||
|
@ -165,6 +168,7 @@ pub enum Issue {
|
||||||
HookInsideLoop(HookInfo, AnyLoopInfo),
|
HookInsideLoop(HookInfo, AnyLoopInfo),
|
||||||
/// <https://dioxuslabs.com/learn/0.5/reference/hooks#no-hooks-in-closures>
|
/// <https://dioxuslabs.com/learn/0.5/reference/hooks#no-hooks-in-closures>
|
||||||
HookInsideClosure(HookInfo, ClosureInfo),
|
HookInsideClosure(HookInfo, ClosureInfo),
|
||||||
|
HookInsideAsync(HookInfo, AsyncInfo),
|
||||||
HookOutsideComponent(HookInfo),
|
HookOutsideComponent(HookInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +178,7 @@ impl Issue {
|
||||||
Issue::HookInsideConditional(hook_info, _)
|
Issue::HookInsideConditional(hook_info, _)
|
||||||
| Issue::HookInsideLoop(hook_info, _)
|
| Issue::HookInsideLoop(hook_info, _)
|
||||||
| Issue::HookInsideClosure(hook_info, _)
|
| Issue::HookInsideClosure(hook_info, _)
|
||||||
|
| Issue::HookInsideAsync(hook_info, _)
|
||||||
| Issue::HookOutsideComponent(hook_info) => hook_info.clone(),
|
| Issue::HookOutsideComponent(hook_info) => hook_info.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,6 +213,9 @@ impl std::fmt::Display for Issue {
|
||||||
Issue::HookInsideClosure(hook_info, _) => {
|
Issue::HookInsideClosure(hook_info, _) => {
|
||||||
write!(f, "hook called in a closure: `{}`", hook_info.name)
|
write!(f, "hook called in a closure: `{}`", hook_info.name)
|
||||||
}
|
}
|
||||||
|
Issue::HookInsideAsync(hook_info, _) => {
|
||||||
|
write!(f, "hook called in an async block: `{}`", hook_info.name)
|
||||||
|
}
|
||||||
Issue::HookOutsideComponent(hook_info) => {
|
Issue::HookOutsideComponent(hook_info) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
|
|
|
@ -111,6 +111,18 @@ impl ClosureInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
/// Information about an async block.
|
||||||
|
pub struct AsyncInfo {
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncInfo {
|
||||||
|
pub const fn new(span: Span) -> Self {
|
||||||
|
Self { span }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Information about a component function.
|
/// Information about a component function.
|
||||||
pub struct ComponentInfo {
|
pub struct ComponentInfo {
|
||||||
|
|
|
@ -424,6 +424,8 @@ impl Scope {
|
||||||
|
|
||||||
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
|
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
|
||||||
Functions prefixed with "use" should never be called conditionally.
|
Functions prefixed with "use" should never be called conditionally.
|
||||||
|
|
||||||
|
Help: Run `dx check` to look for check for some common hook errors.
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue