mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 14:10:20 +00:00
Merge pull request #1215 from eventualbuddha/feat/check/rules-of-hooks
feat(check): adds `dx check`
This commit is contained in:
commit
6751d5941b
13 changed files with 1504 additions and 18 deletions
|
@ -17,6 +17,7 @@ members = [
|
|||
"packages/fermi",
|
||||
"packages/liveview",
|
||||
"packages/autofmt",
|
||||
"packages/check",
|
||||
"packages/rsx",
|
||||
"packages/dioxus-tui",
|
||||
"packages/rink",
|
||||
|
@ -63,6 +64,7 @@ dioxus-interpreter-js = { path = "packages/interpreter" }
|
|||
fermi = { path = "packages/fermi" }
|
||||
dioxus-liveview = { path = "packages/liveview" }
|
||||
dioxus-autofmt = { path = "packages/autofmt" }
|
||||
dioxus-check = { path = "packages/check" }
|
||||
dioxus-rsx = { path = "packages/rsx" }
|
||||
dioxus-tui = { path = "packages/dioxus-tui" }
|
||||
rink = { path = "packages/rink" }
|
||||
|
@ -82,6 +84,9 @@ rustc-hash = "1.1.0"
|
|||
wasm-bindgen = "0.2.87"
|
||||
html_parser = "0.7.0"
|
||||
thiserror = "1.0.40"
|
||||
prettyplease = { package = "prettier-please", version = "0.2", features = [
|
||||
"verbatim",
|
||||
] }
|
||||
|
||||
# This is a "virtual package"
|
||||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||
|
|
|
@ -16,9 +16,7 @@ proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
|||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
prettyplease = { package = "prettier-please", version = "0.2", features = [
|
||||
"verbatim",
|
||||
] }
|
||||
prettyplease = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.2.1"
|
||||
|
|
24
packages/check/Cargo.toml
Normal file
24
packages/check/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "dioxus-check"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Dioxus Labs"]
|
||||
description = "Checks Dioxus RSX files for issues"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-rsx = { workspace = true }
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits", "visit"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||
prettyplease = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "2.0.3"
|
||||
pretty_assertions = "1.2.1"
|
43
packages/check/README.md
Normal file
43
packages/check/README.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
# dioxus-check
|
||||
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-autofmt
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-check` analyzes Dioxus source code and reports errors and warnings. Primarily, it enforces the [Rules of Hooks](https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-conditionals).
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||
terms or conditions.
|
639
packages/check/src/check.rs
Normal file
639
packages/check/src/check.rs
Normal file
|
@ -0,0 +1,639 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use syn::{spanned::Spanned, visit::Visit, Pat};
|
||||
|
||||
use crate::{
|
||||
issues::{Issue, IssueReport},
|
||||
metadata::{
|
||||
AnyLoopInfo, 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,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Node {
|
||||
If(IfInfo),
|
||||
Match(MatchInfo),
|
||||
For(ForInfo),
|
||||
While(WhileInfo),
|
||||
Loop(LoopInfo),
|
||||
Closure(ClosureInfo),
|
||||
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((_, expr)) = &local.init {
|
||||
if let syn::Expr::Closure(closure) = &**expr {
|
||||
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::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
)));
|
||||
syn::visit::visit_expr_if(self, i);
|
||||
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(),
|
||||
)));
|
||||
syn::visit::visit_expr_match(self, i);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
#[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(cx: Scope) -> 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(cx: Scope) -> Element {
|
||||
let count = use_state(cx, || 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(cx: Scope) -> UseState<i32> {
|
||||
use_state(cx, || 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(cx: Scope) -> Element {
|
||||
let use_thing = || {
|
||||
use_state(cx, || 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(cx: Scope) -> Element {
|
||||
if you_are_happy && you_know_it {
|
||||
let something = use_state(cx, || "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_state(cx, || "hands")"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
r#"use_state"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
ConditionalInfo::If(IfInfo::new(
|
||||
Span::new_from_str(
|
||||
"if you_are_happy && you_know_it {\n let something = use_state(cx, || \"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(cx: Scope) -> Element {
|
||||
match you_are_happy && you_know_it {
|
||||
true => {
|
||||
let something = use_state(cx, || "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_state(cx, || "hands")"#, LineColumn { line: 4, column: 28 }),
|
||||
Span::new_from_str(r#"use_state"#, LineColumn { line: 4, column: 28 }),
|
||||
"use_state".to_string()
|
||||
),
|
||||
ConditionalInfo::Match(MatchInfo::new(
|
||||
Span::new_from_str(
|
||||
"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 }",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
),
|
||||
Span::new_from_str("match you_are_happy && you_know_it", LineColumn { line: 2, column: 4 })
|
||||
))
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
for _name in &names {
|
||||
let is_selected = use_state(cx, || 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_state(cx, || false)",
|
||||
LineColumn { line: 3, column: 26 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn { line: 3, column: 26 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
AnyLoopInfo::For(ForInfo::new(
|
||||
Span::new_from_str(
|
||||
"for _name in &names {\n let is_selected = use_state(cx, || 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(cx: Scope) -> Element {
|
||||
while true {
|
||||
let something = use_state(cx, || "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_state(cx, || "hands")"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
AnyLoopInfo::While(WhileInfo::new(
|
||||
Span::new_from_str(
|
||||
"while true {\n let something = use_state(cx, || \"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(cx: Scope) -> Element {
|
||||
loop {
|
||||
let something = use_state(cx, || "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_state(cx, || "hands")"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
AnyLoopInfo::Loop(LoopInfo::new(Span::new_from_str(
|
||||
"loop {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
)))
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conditional_okay() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let something = use_state(cx, || "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_closure_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let _a = || {
|
||||
let b = use_state(cx, || 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_state(cx, || 0)",
|
||||
LineColumn {
|
||||
line: 3,
|
||||
column: 16
|
||||
},
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn {
|
||||
line: 3,
|
||||
column: 16
|
||||
},
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
ClosureInfo::new(Span::new_from_str(
|
||||
"|| {\n let b = use_state(cx, || 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(cx: Scope) {
|
||||
let _a = use_state(cx, || 0);
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookOutsideComponent(HookInfo::new(
|
||||
Span::new_from_str(
|
||||
"use_state(cx, || 0)",
|
||||
LineColumn {
|
||||
line: 2,
|
||||
column: 13
|
||||
}
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn {
|
||||
line: 2,
|
||||
column: 13
|
||||
},
|
||||
),
|
||||
"use_state".to_string()
|
||||
))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hook_inside_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn use_thing(cx: Scope) {
|
||||
let _a = use_state(cx, || 0);
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(report.issues, vec![]);
|
||||
}
|
||||
}
|
427
packages/check/src/issues.rs
Normal file
427
packages/check/src/issues.rs
Normal file
|
@ -0,0 +1,427 @@
|
|||
use owo_colors::{
|
||||
colors::{css::LightBlue, BrightRed},
|
||||
OwoColorize, Stream,
|
||||
};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::metadata::{
|
||||
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo, WhileInfo,
|
||||
};
|
||||
|
||||
/// The result of checking a Dioxus file for issues.
|
||||
pub struct IssueReport {
|
||||
pub path: PathBuf,
|
||||
pub crate_root: PathBuf,
|
||||
pub file_content: String,
|
||||
pub issues: Vec<Issue>,
|
||||
}
|
||||
|
||||
impl IssueReport {
|
||||
pub fn new<S: ToString>(
|
||||
path: PathBuf,
|
||||
crate_root: PathBuf,
|
||||
file_content: S,
|
||||
issues: Vec<Issue>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path,
|
||||
crate_root,
|
||||
file_content: file_content.to_string(),
|
||||
issues,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lightblue(text: &str) -> String {
|
||||
text.if_supports_color(Stream::Stderr, |text| text.fg::<LightBlue>())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn brightred(text: &str) -> String {
|
||||
text.if_supports_color(Stream::Stderr, |text| text.fg::<BrightRed>())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn bold(text: &str) -> String {
|
||||
text.if_supports_color(Stream::Stderr, |text| text.bold())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
impl Display for IssueReport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let relative_file = Path::new(&self.path)
|
||||
.strip_prefix(&self.crate_root)
|
||||
.unwrap_or(Path::new(&self.path))
|
||||
.display();
|
||||
|
||||
let pipe_char = lightblue("|");
|
||||
|
||||
for (i, issue) in self.issues.iter().enumerate() {
|
||||
let hook_info = issue.hook_info();
|
||||
let hook_span = hook_info.span;
|
||||
let hook_name_span = hook_info.name_span;
|
||||
let error_line = format!("{}: {}", brightred("error"), issue);
|
||||
writeln!(f, "{}", bold(&error_line))?;
|
||||
writeln!(
|
||||
f,
|
||||
" {} {}:{}:{}",
|
||||
lightblue("-->"),
|
||||
relative_file,
|
||||
hook_span.start.line,
|
||||
hook_span.start.column + 1
|
||||
)?;
|
||||
let max_line_num_len = hook_span.end.line.to_string().len();
|
||||
writeln!(f, "{:>max_line_num_len$} {}", "", pipe_char)?;
|
||||
for (i, line) in self.file_content.lines().enumerate() {
|
||||
let line_num = i + 1;
|
||||
if line_num >= hook_span.start.line && line_num <= hook_span.end.line {
|
||||
writeln!(
|
||||
f,
|
||||
"{:>max_line_num_len$} {} {}",
|
||||
lightblue(&line_num.to_string()),
|
||||
pipe_char,
|
||||
line,
|
||||
)?;
|
||||
if line_num == hook_span.start.line {
|
||||
let mut caret = String::new();
|
||||
for _ in 0..hook_name_span.start.column {
|
||||
caret.push(' ');
|
||||
}
|
||||
for _ in hook_name_span.start.column..hook_name_span.end.column {
|
||||
caret.push('^');
|
||||
}
|
||||
writeln!(
|
||||
f,
|
||||
"{:>max_line_num_len$} {} {}",
|
||||
"",
|
||||
pipe_char,
|
||||
brightred(&caret),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let note_text_prefix = format!(
|
||||
"{:>max_line_num_len$} {}\n{:>max_line_num_len$} {} note:",
|
||||
"",
|
||||
pipe_char,
|
||||
"",
|
||||
lightblue("=")
|
||||
);
|
||||
|
||||
match issue {
|
||||
Issue::HookInsideConditional(
|
||||
_,
|
||||
ConditionalInfo::If(IfInfo { span: _, head_span }),
|
||||
)
|
||||
| Issue::HookInsideConditional(
|
||||
_,
|
||||
ConditionalInfo::Match(MatchInfo { span: _, head_span }),
|
||||
) => {
|
||||
if let Some(source_text) = &head_span.source_text {
|
||||
writeln!(
|
||||
f,
|
||||
"{} `{} {{ … }}` is the conditional",
|
||||
note_text_prefix, source_text,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Issue::HookInsideLoop(_, AnyLoopInfo::For(ForInfo { span: _, head_span }))
|
||||
| Issue::HookInsideLoop(_, AnyLoopInfo::While(WhileInfo { span: _, head_span })) => {
|
||||
if let Some(source_text) = &head_span.source_text {
|
||||
writeln!(
|
||||
f,
|
||||
"{} `{} {{ … }}` is the loop",
|
||||
note_text_prefix, source_text,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Issue::HookInsideLoop(_, AnyLoopInfo::Loop(_)) => {
|
||||
writeln!(f, "{} `loop {{ … }}` is the loop", note_text_prefix,)?;
|
||||
}
|
||||
Issue::HookOutsideComponent(_) | Issue::HookInsideClosure(_, _) => {}
|
||||
}
|
||||
|
||||
if i < self.issues.len() - 1 {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[allow(clippy::enum_variant_names)] // we'll add non-hook ones in the future
|
||||
/// Issues that might be found via static analysis of a Dioxus file.
|
||||
pub enum Issue {
|
||||
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-conditionals
|
||||
HookInsideConditional(HookInfo, ConditionalInfo),
|
||||
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-loops
|
||||
HookInsideLoop(HookInfo, AnyLoopInfo),
|
||||
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-closures
|
||||
HookInsideClosure(HookInfo, ClosureInfo),
|
||||
HookOutsideComponent(HookInfo),
|
||||
}
|
||||
|
||||
impl Issue {
|
||||
pub fn hook_info(&self) -> HookInfo {
|
||||
match self {
|
||||
Issue::HookInsideConditional(hook_info, _)
|
||||
| Issue::HookInsideLoop(hook_info, _)
|
||||
| Issue::HookInsideClosure(hook_info, _)
|
||||
| Issue::HookOutsideComponent(hook_info) => hook_info.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Issue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Issue::HookInsideConditional(hook_info, conditional_info) => {
|
||||
write!(
|
||||
f,
|
||||
"hook called conditionally: `{}` (inside `{}`)",
|
||||
hook_info.name,
|
||||
match conditional_info {
|
||||
ConditionalInfo::If(_) => "if",
|
||||
ConditionalInfo::Match(_) => "match",
|
||||
}
|
||||
)
|
||||
}
|
||||
Issue::HookInsideLoop(hook_info, loop_info) => {
|
||||
write!(
|
||||
f,
|
||||
"hook called in a loop: `{}` (inside {})",
|
||||
hook_info.name,
|
||||
match loop_info {
|
||||
AnyLoopInfo::For(_) => "`for` loop",
|
||||
AnyLoopInfo::While(_) => "`while` loop",
|
||||
AnyLoopInfo::Loop(_) => "`loop`",
|
||||
}
|
||||
)
|
||||
}
|
||||
Issue::HookInsideClosure(hook_info, _) => {
|
||||
write!(f, "hook called in a closure: `{}`", hook_info.name)
|
||||
}
|
||||
Issue::HookOutsideComponent(hook_info) => {
|
||||
write!(
|
||||
f,
|
||||
"hook called outside component or hook: `{}`",
|
||||
hook_info.name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::check_file;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_conditional_if() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
if you_are_happy && you_know_it {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called conditionally: `use_state` (inside `if`)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `if you_are_happy && you_know_it { … }` is the conditional
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_conditional_match() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
match you_are_happy && you_know_it {
|
||||
true => {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called conditionally: `use_state` (inside `match`)
|
||||
--> src/main.rs:4:29
|
||||
|
|
||||
4 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `match you_are_happy && you_know_it { … }` is the conditional
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_for_loop() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
for i in 0..10 {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a loop: `use_state` (inside `for` loop)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `for i in 0..10 { … }` is the loop
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_while_loop() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
while check_thing() {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a loop: `use_state` (inside `while` loop)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `while check_thing() { … }` is the loop
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_loop() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
loop {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a loop: `use_state` (inside `loop`)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `loop { … }` is the loop
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_closure() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let something = || {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
};
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a closure: `use_state`
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_multiline_hook() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
if you_are_happy && you_know_it {
|
||||
let something = use_state(cx, || {
|
||||
"hands"
|
||||
});
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called conditionally: `use_state` (inside `if`)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || {
|
||||
| ^^^^^^^^^
|
||||
4 | "hands"
|
||||
5 | });
|
||||
|
|
||||
= note: `if you_are_happy && you_know_it { … }` is the conditional
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
}
|
6
packages/check/src/lib.rs
Normal file
6
packages/check/src/lib.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
mod check;
|
||||
mod issues;
|
||||
mod metadata;
|
||||
|
||||
pub use check::check_file;
|
||||
pub use issues::{Issue, IssueReport};
|
202
packages/check/src/metadata.rs
Normal file
202
packages/check/src/metadata.rs
Normal file
|
@ -0,0 +1,202 @@
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a hook call or function.
|
||||
pub struct HookInfo {
|
||||
/// The name of the hook, e.g. `use_state`.
|
||||
pub name: String,
|
||||
/// The span of the hook, e.g. `use_state(cx, || 0)`.
|
||||
pub span: Span,
|
||||
/// The span of the name, e.g. `use_state`.
|
||||
pub name_span: Span,
|
||||
}
|
||||
|
||||
impl HookInfo {
|
||||
pub const fn new(span: Span, name_span: Span, name: String) -> Self {
|
||||
Self {
|
||||
span,
|
||||
name_span,
|
||||
name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ConditionalInfo {
|
||||
If(IfInfo),
|
||||
Match(MatchInfo),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IfInfo {
|
||||
/// The span of the `if` statement, e.g. `if true { ... }`.
|
||||
pub span: Span,
|
||||
/// The span of the `if true` part only.
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl IfInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MatchInfo {
|
||||
/// The span of the `match` statement, e.g. `match true { ... }`.
|
||||
pub span: Span,
|
||||
/// The span of the `match true` part only.
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl MatchInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about one of the possible loop types.
|
||||
pub enum AnyLoopInfo {
|
||||
For(ForInfo),
|
||||
While(WhileInfo),
|
||||
Loop(LoopInfo),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a `for` loop.
|
||||
pub struct ForInfo {
|
||||
pub span: Span,
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl ForInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a `while` loop.
|
||||
pub struct WhileInfo {
|
||||
pub span: Span,
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl WhileInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a `loop` loop.
|
||||
pub struct LoopInfo {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl LoopInfo {
|
||||
pub const fn new(span: Span) -> Self {
|
||||
Self { span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a closure.
|
||||
pub struct ClosureInfo {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl ClosureInfo {
|
||||
pub const fn new(span: Span) -> Self {
|
||||
Self { span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a component function.
|
||||
pub struct ComponentInfo {
|
||||
pub span: Span,
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
}
|
||||
|
||||
impl ComponentInfo {
|
||||
pub const fn new(span: Span, name: String, name_span: Span) -> Self {
|
||||
Self {
|
||||
span,
|
||||
name,
|
||||
name_span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a non-component, non-hook function.
|
||||
pub struct FnInfo {
|
||||
pub span: Span,
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
}
|
||||
|
||||
impl FnInfo {
|
||||
pub const fn new(span: Span, name: String, name_span: Span) -> Self {
|
||||
Self {
|
||||
span,
|
||||
name,
|
||||
name_span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A span of text in a source code file.
|
||||
pub struct Span {
|
||||
pub source_text: Option<String>,
|
||||
pub start: LineColumn,
|
||||
pub end: LineColumn,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
|
||||
let mut lines = source_text.lines();
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
let mut end = LineColumn {
|
||||
line: start.line,
|
||||
column: start.column + first_line.len(),
|
||||
};
|
||||
for line in lines {
|
||||
end.line += 1;
|
||||
end.column = line.len();
|
||||
}
|
||||
Self {
|
||||
source_text: Some(source_text.to_string()),
|
||||
start,
|
||||
end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proc_macro2::Span> for Span {
|
||||
fn from(span: proc_macro2::Span) -> Self {
|
||||
Self {
|
||||
source_text: span.source_text(),
|
||||
start: span.start().into(),
|
||||
end: span.end().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A location in a source code file.
|
||||
pub struct LineColumn {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl From<proc_macro2::LineColumn> for LineColumn {
|
||||
fn from(lc: proc_macro2::LineColumn) -> Self {
|
||||
Self {
|
||||
line: lc.line,
|
||||
column: lc.column,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -77,6 +77,7 @@ toml_edit = "0.19.11"
|
|||
# dioxus-rsx = "0.0.1"
|
||||
|
||||
dioxus-autofmt = { workspace = true }
|
||||
dioxus-check = { workspace = true }
|
||||
rsx-rosetta = { workspace = true }
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
|
||||
|
|
|
@ -5,22 +5,25 @@ In this chapter we will introduce all `dioxus-cli` commands.
|
|||
> You can also use `dx --help` to get cli help info.
|
||||
|
||||
```
|
||||
dx
|
||||
Build, bundle, & ship your Dioxus app
|
||||
Build, Bundle & Ship Dioxus Apps
|
||||
|
||||
USAGE:
|
||||
dx [OPTIONS] <SUBCOMMAND>
|
||||
Usage: dx [OPTIONS] <COMMAND>
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Print help information
|
||||
-v Enable verbose logging
|
||||
Commands:
|
||||
build Build the Rust WASM app and all of its assets
|
||||
translate Translate some source file into Dioxus code
|
||||
serve Build, watch & serve the Rust WASM app and all of its assets
|
||||
create Init a new project for Dioxus
|
||||
clean Clean output artifacts
|
||||
version Print the version of this extension
|
||||
fmt Format some rsx
|
||||
check Check the Rust files in the project for issues
|
||||
config Dioxus config file controls
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
SUBCOMMANDS:
|
||||
build Build the Dioxus application and all of its assets
|
||||
clean Clean output artifacts
|
||||
config Dioxus config file controls
|
||||
create Init a new project for Dioxus
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
serve Build, watch & serve the Rust WASM app and all of its assets
|
||||
translate Translate some html file into a Dioxus component
|
||||
Options:
|
||||
-v Enable verbose logging
|
||||
--bin <BIN> Specify bin target
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
|
128
packages/cli/src/cli/check/mod.rs
Normal file
128
packages/cli/src/cli/check/mod.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{path::Path, process::exit};
|
||||
|
||||
use super::*;
|
||||
|
||||
// For reference, the rustfmt main.rs file
|
||||
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
|
||||
|
||||
/// Check the Rust files in the project for issues.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub struct Check {
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Check {
|
||||
// Todo: check the entire crate
|
||||
pub async fn check(self) -> Result<()> {
|
||||
match self.file {
|
||||
// Default to checking the project
|
||||
None => {
|
||||
if let Err(e) = check_project_and_report().await {
|
||||
eprintln!("error checking project: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
Some(file) => {
|
||||
if let Err(e) = check_file_and_report(file).await {
|
||||
eprintln!("failed to check file: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_file_and_report(path: PathBuf) -> Result<()> {
|
||||
check_files_and_report(vec![path]).await
|
||||
}
|
||||
|
||||
/// Read every .rs file accessible when considering the .gitignore and check it
|
||||
///
|
||||
/// Runs using Tokio for multithreading, so it should be really really fast
|
||||
///
|
||||
/// Doesn't do mod-descending, so it will still try to check unreachable files. TODO.
|
||||
async fn check_project_and_report() -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new(None)?;
|
||||
|
||||
let mut files_to_check = vec![];
|
||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_check);
|
||||
check_files_and_report(files_to_check).await
|
||||
}
|
||||
|
||||
/// Check a list of files and report the issues.
|
||||
async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
|
||||
let issue_reports = files_to_check
|
||||
.into_iter()
|
||||
.filter(|file| file.components().all(|f| f.as_os_str() != "target"))
|
||||
.map(|path| async move {
|
||||
let _path = path.clone();
|
||||
let res = tokio::spawn(async move {
|
||||
tokio::fs::read_to_string(&_path)
|
||||
.await
|
||||
.map(|contents| dioxus_check::check_file(_path, &contents))
|
||||
})
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
eprintln!("error checking file: {}", path.display());
|
||||
}
|
||||
|
||||
res
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
// remove error results which we've already printed
|
||||
let issue_reports = issue_reports
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total_issues = issue_reports.iter().map(|r| r.issues.len()).sum::<usize>();
|
||||
|
||||
for report in issue_reports.into_iter() {
|
||||
if !report.issues.is_empty() {
|
||||
println!("{}", report);
|
||||
}
|
||||
}
|
||||
|
||||
match total_issues {
|
||||
0 => println!("No issues found."),
|
||||
1 => println!("1 issue found."),
|
||||
_ => println!("{} issues found.", total_issues),
|
||||
}
|
||||
|
||||
match total_issues {
|
||||
0 => exit(0),
|
||||
_ => exit(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else { return };
|
||||
|
||||
// load the gitignore
|
||||
|
||||
for entry in folder {
|
||||
let Ok(entry) = entry else { continue; };
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
collect_rs_files(&path, files);
|
||||
}
|
||||
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "rs" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
pub mod autoformat;
|
||||
pub mod build;
|
||||
pub mod cfg;
|
||||
pub mod check;
|
||||
pub mod clean;
|
||||
pub mod config;
|
||||
pub mod create;
|
||||
|
@ -67,6 +68,9 @@ pub enum Commands {
|
|||
#[clap(name = "fmt")]
|
||||
Autoformat(autoformat::Autoformat),
|
||||
|
||||
#[clap(name = "check")]
|
||||
Check(check::Check),
|
||||
|
||||
/// Dioxus config file controls.
|
||||
#[clap(subcommand)]
|
||||
Config(config::Config),
|
||||
|
@ -88,6 +92,7 @@ impl Display for Commands {
|
|||
Commands::Config(_) => write!(f, "config"),
|
||||
Commands::Version(_) => write!(f, "version"),
|
||||
Commands::Autoformat(_) => write!(f, "fmt"),
|
||||
Commands::Check(_) => write!(f, "check"),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
Commands::Plugin(_) => write!(f, "plugin"),
|
||||
|
|
|
@ -103,6 +103,11 @@ async fn main() -> anyhow::Result<()> {
|
|||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error autoformatting RSX: {}", e)),
|
||||
|
||||
Check(opts) => opts
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error checking RSX: {}", e)),
|
||||
|
||||
Version(opt) => {
|
||||
let version = opt.version();
|
||||
println!("{}", version);
|
||||
|
|
Loading…
Reference in a new issue