* 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
844 lines
27 KiB
Rust
844 lines
27 KiB
Rust
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,
|
|
},
|
|
))
|
|
),]
|
|
);
|
|
}
|
|
}
|