check.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. use std::path::PathBuf;
  2. use syn::{spanned::Spanned, visit::Visit, Pat};
  3. use crate::{
  4. issues::{Issue, IssueReport},
  5. metadata::{
  6. AnyLoopInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo, HookInfo,
  7. 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. #[derive(Debug, Clone)]
  35. enum Node {
  36. If(IfInfo),
  37. Match(MatchInfo),
  38. For(ForInfo),
  39. While(WhileInfo),
  40. Loop(LoopInfo),
  41. Closure(ClosureInfo),
  42. ComponentFn(ComponentInfo),
  43. HookFn(HookInfo),
  44. OtherFn(FnInfo),
  45. }
  46. fn returns_element(ty: &syn::ReturnType) -> bool {
  47. match ty {
  48. syn::ReturnType::Default => false,
  49. syn::ReturnType::Type(_, ref ty) => {
  50. if let syn::Type::Path(ref path) = **ty {
  51. if let Some(segment) = path.path.segments.last() {
  52. if segment.ident == "Element" {
  53. return true;
  54. }
  55. }
  56. }
  57. false
  58. }
  59. }
  60. }
  61. fn is_hook_ident(ident: &syn::Ident) -> bool {
  62. ident.to_string().starts_with("use_")
  63. }
  64. fn is_component_fn(item_fn: &syn::ItemFn) -> bool {
  65. returns_element(&item_fn.sig.output)
  66. }
  67. fn get_closure_hook_body(local: &syn::Local) -> Option<&syn::Expr> {
  68. if let Pat::Ident(ident) = &local.pat {
  69. if is_hook_ident(&ident.ident) {
  70. if let Some((_, expr)) = &local.init {
  71. if let syn::Expr::Closure(closure) = &**expr {
  72. return Some(&closure.body);
  73. }
  74. }
  75. }
  76. }
  77. None
  78. }
  79. fn fn_name_and_name_span(item_fn: &syn::ItemFn) -> (String, Span) {
  80. let name = item_fn.sig.ident.to_string();
  81. let name_span = item_fn.sig.ident.span().into();
  82. (name, name_span)
  83. }
  84. impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
  85. fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) {
  86. if let syn::Expr::Path(ref path) = *i.func {
  87. if let Some(segment) = path.path.segments.last() {
  88. if is_hook_ident(&segment.ident) {
  89. let hook_info = HookInfo::new(
  90. i.span().into(),
  91. segment.ident.span().into(),
  92. segment.ident.to_string(),
  93. );
  94. let mut container_fn: Option<Node> = None;
  95. for node in self.context.iter().rev() {
  96. match node {
  97. Node::If(if_info) => {
  98. let issue = Issue::HookInsideConditional(
  99. hook_info.clone(),
  100. ConditionalInfo::If(if_info.clone()),
  101. );
  102. self.issues.push(issue);
  103. }
  104. Node::Match(match_info) => {
  105. let issue = Issue::HookInsideConditional(
  106. hook_info.clone(),
  107. ConditionalInfo::Match(match_info.clone()),
  108. );
  109. self.issues.push(issue);
  110. }
  111. Node::For(for_info) => {
  112. let issue = Issue::HookInsideLoop(
  113. hook_info.clone(),
  114. AnyLoopInfo::For(for_info.clone()),
  115. );
  116. self.issues.push(issue);
  117. }
  118. Node::While(while_info) => {
  119. let issue = Issue::HookInsideLoop(
  120. hook_info.clone(),
  121. AnyLoopInfo::While(while_info.clone()),
  122. );
  123. self.issues.push(issue);
  124. }
  125. Node::Loop(loop_info) => {
  126. let issue = Issue::HookInsideLoop(
  127. hook_info.clone(),
  128. AnyLoopInfo::Loop(loop_info.clone()),
  129. );
  130. self.issues.push(issue);
  131. }
  132. Node::Closure(closure_info) => {
  133. let issue = Issue::HookInsideClosure(
  134. hook_info.clone(),
  135. closure_info.clone(),
  136. );
  137. self.issues.push(issue);
  138. }
  139. Node::ComponentFn(_) | Node::HookFn(_) | Node::OtherFn(_) => {
  140. container_fn = Some(node.clone());
  141. break;
  142. }
  143. }
  144. }
  145. if let Some(Node::OtherFn(_)) = container_fn {
  146. let issue = Issue::HookOutsideComponent(hook_info);
  147. self.issues.push(issue);
  148. }
  149. }
  150. }
  151. }
  152. }
  153. fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
  154. let (name, name_span) = fn_name_and_name_span(i);
  155. if is_component_fn(i) {
  156. self.context.push(Node::ComponentFn(ComponentInfo::new(
  157. i.span().into(),
  158. name,
  159. name_span,
  160. )));
  161. } else if is_hook_ident(&i.sig.ident) {
  162. self.context.push(Node::HookFn(HookInfo::new(
  163. i.span().into(),
  164. i.sig.ident.span().into(),
  165. name,
  166. )));
  167. } else {
  168. self.context
  169. .push(Node::OtherFn(FnInfo::new(i.span().into(), name, name_span)));
  170. }
  171. syn::visit::visit_item_fn(self, i);
  172. self.context.pop();
  173. }
  174. fn visit_local(&mut self, i: &'ast syn::Local) {
  175. if let Some(body) = get_closure_hook_body(i) {
  176. // if the closure is a hook, we only visit the body of the closure.
  177. // this prevents adding a ClosureInfo node to the context
  178. syn::visit::visit_expr(self, body);
  179. } else {
  180. // otherwise visit the whole local
  181. syn::visit::visit_local(self, i);
  182. }
  183. }
  184. fn visit_expr_if(&mut self, i: &'ast syn::ExprIf) {
  185. self.context.push(Node::If(IfInfo::new(
  186. i.span().into(),
  187. i.if_token
  188. .span()
  189. .join(i.cond.span())
  190. .unwrap_or_else(|| i.span())
  191. .into(),
  192. )));
  193. syn::visit::visit_expr_if(self, i);
  194. self.context.pop();
  195. }
  196. fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
  197. self.context.push(Node::Match(MatchInfo::new(
  198. i.span().into(),
  199. i.match_token
  200. .span()
  201. .join(i.expr.span())
  202. .unwrap_or_else(|| i.span())
  203. .into(),
  204. )));
  205. syn::visit::visit_expr_match(self, i);
  206. self.context.pop();
  207. }
  208. fn visit_expr_for_loop(&mut self, i: &'ast syn::ExprForLoop) {
  209. self.context.push(Node::For(ForInfo::new(
  210. i.span().into(),
  211. i.for_token
  212. .span()
  213. .join(i.expr.span())
  214. .unwrap_or_else(|| i.span())
  215. .into(),
  216. )));
  217. syn::visit::visit_expr_for_loop(self, i);
  218. self.context.pop();
  219. }
  220. fn visit_expr_while(&mut self, i: &'ast syn::ExprWhile) {
  221. self.context.push(Node::While(WhileInfo::new(
  222. i.span().into(),
  223. i.while_token
  224. .span()
  225. .join(i.cond.span())
  226. .unwrap_or_else(|| i.span())
  227. .into(),
  228. )));
  229. syn::visit::visit_expr_while(self, i);
  230. self.context.pop();
  231. }
  232. fn visit_expr_loop(&mut self, i: &'ast syn::ExprLoop) {
  233. self.context
  234. .push(Node::Loop(LoopInfo::new(i.span().into())));
  235. syn::visit::visit_expr_loop(self, i);
  236. self.context.pop();
  237. }
  238. fn visit_expr_closure(&mut self, i: &'ast syn::ExprClosure) {
  239. self.context
  240. .push(Node::Closure(ClosureInfo::new(i.span().into())));
  241. syn::visit::visit_expr_closure(self, i);
  242. self.context.pop();
  243. }
  244. }
  245. #[cfg(test)]
  246. mod tests {
  247. use crate::metadata::{
  248. AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
  249. MatchInfo, Span, WhileInfo,
  250. };
  251. use indoc::indoc;
  252. use pretty_assertions::assert_eq;
  253. use super::*;
  254. #[test]
  255. fn test_no_hooks() {
  256. let contents = indoc! {r#"
  257. fn App(cx: Scope) -> Element {
  258. rsx! {
  259. p { "Hello World" }
  260. }
  261. }
  262. "#};
  263. let report = check_file("app.rs".into(), contents);
  264. assert_eq!(report.issues, vec![]);
  265. }
  266. #[test]
  267. fn test_hook_correctly_used_inside_component() {
  268. let contents = indoc! {r#"
  269. fn App(cx: Scope) -> Element {
  270. let count = use_state(cx, || 0);
  271. rsx! {
  272. p { "Hello World: {count}" }
  273. }
  274. }
  275. "#};
  276. let report = check_file("app.rs".into(), contents);
  277. assert_eq!(report.issues, vec![]);
  278. }
  279. #[test]
  280. fn test_hook_correctly_used_inside_hook_fn() {
  281. let contents = indoc! {r#"
  282. fn use_thing(cx: Scope) -> UseState<i32> {
  283. use_state(cx, || 0)
  284. }
  285. "#};
  286. let report = check_file("use_thing.rs".into(), contents);
  287. assert_eq!(report.issues, vec![]);
  288. }
  289. #[test]
  290. fn test_hook_correctly_used_inside_hook_closure() {
  291. let contents = indoc! {r#"
  292. fn App(cx: Scope) -> Element {
  293. let use_thing = || {
  294. use_state(cx, || 0)
  295. };
  296. let count = use_thing();
  297. rsx! {
  298. p { "Hello World: {count}" }
  299. }
  300. }
  301. "#};
  302. let report = check_file("app.rs".into(), contents);
  303. assert_eq!(report.issues, vec![]);
  304. }
  305. #[test]
  306. fn test_conditional_hook_if() {
  307. let contents = indoc! {r#"
  308. fn App(cx: Scope) -> Element {
  309. if you_are_happy && you_know_it {
  310. let something = use_state(cx, || "hands");
  311. println!("clap your {something}")
  312. }
  313. }
  314. "#};
  315. let report = check_file("app.rs".into(), contents);
  316. assert_eq!(
  317. report.issues,
  318. vec![Issue::HookInsideConditional(
  319. HookInfo::new(
  320. Span::new_from_str(
  321. r#"use_state(cx, || "hands")"#,
  322. LineColumn { line: 3, column: 24 },
  323. ),
  324. Span::new_from_str(
  325. r#"use_state"#,
  326. LineColumn { line: 3, column: 24 },
  327. ),
  328. "use_state".to_string()
  329. ),
  330. ConditionalInfo::If(IfInfo::new(
  331. Span::new_from_str(
  332. "if you_are_happy && you_know_it {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
  333. LineColumn { line: 2, column: 4 },
  334. ),
  335. Span::new_from_str(
  336. "if you_are_happy && you_know_it",
  337. LineColumn { line: 2, column: 4 }
  338. )
  339. ))
  340. )],
  341. );
  342. }
  343. #[test]
  344. fn test_conditional_hook_match() {
  345. let contents = indoc! {r#"
  346. fn App(cx: Scope) -> Element {
  347. match you_are_happy && you_know_it {
  348. true => {
  349. let something = use_state(cx, || "hands");
  350. println!("clap your {something}")
  351. }
  352. false => {}
  353. }
  354. }
  355. "#};
  356. let report = check_file("app.rs".into(), contents);
  357. assert_eq!(
  358. report.issues,
  359. vec![Issue::HookInsideConditional(
  360. HookInfo::new(
  361. Span::new_from_str(r#"use_state(cx, || "hands")"#, LineColumn { line: 4, column: 28 }),
  362. Span::new_from_str(r#"use_state"#, LineColumn { line: 4, column: 28 }),
  363. "use_state".to_string()
  364. ),
  365. ConditionalInfo::Match(MatchInfo::new(
  366. Span::new_from_str(
  367. "match you_are_happy && you_know_it {\n true => {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }\n false => {}\n }",
  368. LineColumn { line: 2, column: 4 },
  369. ),
  370. Span::new_from_str("match you_are_happy && you_know_it", LineColumn { line: 2, column: 4 })
  371. ))
  372. )]
  373. );
  374. }
  375. #[test]
  376. fn test_for_loop_hook() {
  377. let contents = indoc! {r#"
  378. fn App(cx: Scope) -> Element {
  379. for _name in &names {
  380. let is_selected = use_state(cx, || false);
  381. println!("selected: {is_selected}");
  382. }
  383. }
  384. "#};
  385. let report = check_file("app.rs".into(), contents);
  386. assert_eq!(
  387. report.issues,
  388. vec![Issue::HookInsideLoop(
  389. HookInfo::new(
  390. Span::new_from_str(
  391. "use_state(cx, || false)",
  392. LineColumn { line: 3, column: 26 },
  393. ),
  394. Span::new_from_str(
  395. "use_state",
  396. LineColumn { line: 3, column: 26 },
  397. ),
  398. "use_state".to_string()
  399. ),
  400. AnyLoopInfo::For(ForInfo::new(
  401. Span::new_from_str(
  402. "for _name in &names {\n let is_selected = use_state(cx, || false);\n println!(\"selected: {is_selected}\");\n }",
  403. LineColumn { line: 2, column: 4 },
  404. ),
  405. Span::new_from_str(
  406. "for _name in &names",
  407. LineColumn { line: 2, column: 4 },
  408. )
  409. ))
  410. )]
  411. );
  412. }
  413. #[test]
  414. fn test_while_loop_hook() {
  415. let contents = indoc! {r#"
  416. fn App(cx: Scope) -> Element {
  417. while true {
  418. let something = use_state(cx, || "hands");
  419. println!("clap your {something}")
  420. }
  421. }
  422. "#};
  423. let report = check_file("app.rs".into(), contents);
  424. assert_eq!(
  425. report.issues,
  426. vec![Issue::HookInsideLoop(
  427. HookInfo::new(
  428. Span::new_from_str(
  429. r#"use_state(cx, || "hands")"#,
  430. LineColumn { line: 3, column: 24 },
  431. ),
  432. Span::new_from_str(
  433. "use_state",
  434. LineColumn { line: 3, column: 24 },
  435. ),
  436. "use_state".to_string()
  437. ),
  438. AnyLoopInfo::While(WhileInfo::new(
  439. Span::new_from_str(
  440. "while true {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
  441. LineColumn { line: 2, column: 4 },
  442. ),
  443. Span::new_from_str(
  444. "while true",
  445. LineColumn { line: 2, column: 4 },
  446. )
  447. ))
  448. )],
  449. );
  450. }
  451. #[test]
  452. fn test_loop_hook() {
  453. let contents = indoc! {r#"
  454. fn App(cx: Scope) -> Element {
  455. loop {
  456. let something = use_state(cx, || "hands");
  457. println!("clap your {something}")
  458. }
  459. }
  460. "#};
  461. let report = check_file("app.rs".into(), contents);
  462. assert_eq!(
  463. report.issues,
  464. vec![Issue::HookInsideLoop(
  465. HookInfo::new(
  466. Span::new_from_str(
  467. r#"use_state(cx, || "hands")"#,
  468. LineColumn { line: 3, column: 24 },
  469. ),
  470. Span::new_from_str(
  471. "use_state",
  472. LineColumn { line: 3, column: 24 },
  473. ),
  474. "use_state".to_string()
  475. ),
  476. AnyLoopInfo::Loop(LoopInfo::new(Span::new_from_str(
  477. "loop {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
  478. LineColumn { line: 2, column: 4 },
  479. )))
  480. )],
  481. );
  482. }
  483. #[test]
  484. fn test_conditional_okay() {
  485. let contents = indoc! {r#"
  486. fn App(cx: Scope) -> Element {
  487. let something = use_state(cx, || "hands");
  488. if you_are_happy && you_know_it {
  489. println!("clap your {something}")
  490. }
  491. }
  492. "#};
  493. let report = check_file("app.rs".into(), contents);
  494. assert_eq!(report.issues, vec![]);
  495. }
  496. #[test]
  497. fn test_closure_hook() {
  498. let contents = indoc! {r#"
  499. fn App(cx: Scope) -> Element {
  500. let _a = || {
  501. let b = use_state(cx, || 0);
  502. b.get()
  503. };
  504. }
  505. "#};
  506. let report = check_file("app.rs".into(), contents);
  507. assert_eq!(
  508. report.issues,
  509. vec![Issue::HookInsideClosure(
  510. HookInfo::new(
  511. Span::new_from_str(
  512. "use_state(cx, || 0)",
  513. LineColumn {
  514. line: 3,
  515. column: 16
  516. },
  517. ),
  518. Span::new_from_str(
  519. "use_state",
  520. LineColumn {
  521. line: 3,
  522. column: 16
  523. },
  524. ),
  525. "use_state".to_string()
  526. ),
  527. ClosureInfo::new(Span::new_from_str(
  528. "|| {\n let b = use_state(cx, || 0);\n b.get()\n }",
  529. LineColumn {
  530. line: 2,
  531. column: 13
  532. },
  533. ))
  534. )]
  535. );
  536. }
  537. #[test]
  538. fn test_hook_outside_component() {
  539. let contents = indoc! {r#"
  540. fn not_component_or_hook(cx: Scope) {
  541. let _a = use_state(cx, || 0);
  542. }
  543. "#};
  544. let report = check_file("app.rs".into(), contents);
  545. assert_eq!(
  546. report.issues,
  547. vec![Issue::HookOutsideComponent(HookInfo::new(
  548. Span::new_from_str(
  549. "use_state(cx, || 0)",
  550. LineColumn {
  551. line: 2,
  552. column: 13
  553. }
  554. ),
  555. Span::new_from_str(
  556. "use_state",
  557. LineColumn {
  558. line: 2,
  559. column: 13
  560. },
  561. ),
  562. "use_state".to_string()
  563. ))]
  564. );
  565. }
  566. #[test]
  567. fn test_hook_inside_hook() {
  568. let contents = indoc! {r#"
  569. fn use_thing(cx: Scope) {
  570. let _a = use_state(cx, || 0);
  571. }
  572. "#};
  573. let report = check_file("app.rs".into(), contents);
  574. assert_eq!(report.issues, vec![]);
  575. }
  576. }