check.rs 27 KB

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