feat(check): adds dx check

At the moment this only checks the Rules of Hooks, ensuring that hook functions (i.e. `use_*`) are being called as expected.

https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html

Closes #1202
This commit is contained in:
Brian Donovan 2023-07-18 17:12:47 -04:00
parent 915e79e3be
commit 2c2534d1cc
No known key found for this signature in database
11 changed files with 1255 additions and 0 deletions

View file

@ -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" }

25
packages/check/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[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"] }
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
"verbatim",
] }
owo-colors = "3.5.0"
[dev-dependencies]
pretty_assertions = "1.2.1"

49
packages/check/README.md Normal file
View file

@ -0,0 +1,49 @@
# dioxus-autofmt
[![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-autofmt` provides a pretty printer for the `rsx` syntax tree.
This is done manually with a via set of formatting rules. The output is not guaranteed to be stable between minor versions of the crate as we might tweak the output.
`dioxus-autofmt` provides an API to perform precision edits as well as just spit out a block of formatted RSX from any RSX syntax tree. This is used by the `rsx-rosetta` crate which can accept various input languages and output valid RSX.
## 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.

226
packages/check/src/check.rs Normal file
View file

@ -0,0 +1,226 @@
use std::path::PathBuf;
use syn::{spanned::Spanned, visit::Visit};
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 {
path,
file_content: file_content.to_string(),
issues: 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 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_expr_if(&mut self, i: &'ast syn::ExprIf) {
self.context.push(Node::If(IfInfo::new(
i.span().into(),
i.if_token.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().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().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().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(),
i.loop_token.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();
}
}

View file

@ -0,0 +1,154 @@
use owo_colors::{
colors::{css::LightBlue, BrightRed},
OwoColorize,
};
use std::{
fmt::Display,
path::{Path, PathBuf},
};
use crate::metadata::{AnyLoopInfo, ClosureInfo, ConditionalInfo, HookInfo};
/// The result of checking a Dioxus file for issues.
pub struct IssueReport {
pub path: PathBuf,
pub file_content: String,
pub issues: Vec<Issue>,
}
impl IssueReport {
pub fn new<S: ToString>(path: PathBuf, file_content: S, issues: Vec<Issue>) -> Self {
Self {
path,
file_content: file_content.to_string(),
issues,
}
}
}
impl Display for IssueReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let relative_file = Path::new(&self.path)
.strip_prefix(std::env::current_dir().unwrap())
.unwrap_or(Path::new(&self.path))
.display();
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!("{}: {}", "error".fg::<BrightRed>(), issue);
writeln!(f, "{}", error_line.bold())?;
writeln!(
f,
" {} {}:{}:{}",
"-->".fg::<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$} {}", "", "|".fg::<LightBlue>())?;
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$} {} {}",
line_num,
"|".fg::<LightBlue>(),
line,
max_line_num_len = max_line_num_len
)?;
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$} {} {}",
"",
"|".fg::<LightBlue>(),
caret.fg::<BrightRed>(),
max_line_num_len = max_line_num_len
)?;
}
}
}
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 inside closure: `{}`", hook_info.name)
}
Issue::HookOutsideComponent(hook_info) => {
write!(
f,
"hook called outside component or hook: `{}`",
hook_info.name
)
}
}
}
}

479
packages/check/src/lib.rs Normal file
View file

@ -0,0 +1,479 @@
mod check;
mod issues;
mod metadata;
pub use check::check_file;
pub use issues::{Issue, IssueReport};
#[cfg(test)]
mod tests {
use crate::metadata::{
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
MatchInfo, Span, WhileInfo,
};
use super::*;
#[test]
fn test_no_issues() {
let contents = 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_conditional_hook_if() {
let contents = 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 {
start: LineColumn {
line: 4,
column: 36
},
end: LineColumn {
line: 4,
column: 61
}
},
Span {
start: LineColumn {
line: 4,
column: 36
},
end: LineColumn {
line: 4,
column: 45
}
},
"use_state".to_string()
),
ConditionalInfo::If(IfInfo::new(
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 6,
column: 17
}
},
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 3,
column: 18
}
}
))
)],
);
}
#[test]
fn test_conditional_hook_match() {
let contents = 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 {
start: LineColumn {
line: 5,
column: 40
},
end: LineColumn {
line: 5,
column: 65
}
},
Span {
start: LineColumn {
line: 5,
column: 40
},
end: LineColumn {
line: 5,
column: 49
}
},
"use_state".to_string()
),
ConditionalInfo::Match(MatchInfo::new(
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 9,
column: 17
}
},
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 3,
column: 21
}
}
))
)]
);
}
#[test]
fn test_for_loop_hook() {
let contents = 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 {
start: LineColumn {
line: 4,
column: 38
},
end: LineColumn {
line: 4,
column: 61
}
},
Span {
start: LineColumn {
line: 4,
column: 38
},
end: LineColumn {
line: 4,
column: 47
}
},
"use_state".to_string()
),
AnyLoopInfo::For(ForInfo::new(
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 6,
column: 17
}
},
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 3,
column: 19
}
}
))
)]
);
}
#[test]
fn test_while_loop_hook() {
let contents = 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 {
start: LineColumn {
line: 4,
column: 36
},
end: LineColumn {
line: 4,
column: 61
}
},
Span {
start: LineColumn {
line: 4,
column: 36
},
end: LineColumn {
line: 4,
column: 45
}
},
"use_state".to_string()
),
AnyLoopInfo::While(WhileInfo::new(
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 6,
column: 17
}
},
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 3,
column: 21
}
}
))
)],
);
}
#[test]
fn test_loop_hook() {
let contents = 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 {
start: LineColumn {
line: 4,
column: 36
},
end: LineColumn {
line: 4,
column: 61
}
},
Span {
start: LineColumn {
line: 4,
column: 36
},
end: LineColumn {
line: 4,
column: 45
}
},
"use_state".to_string()
),
AnyLoopInfo::Loop(LoopInfo::new(
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 6,
column: 17
}
},
Span {
start: LineColumn {
line: 3,
column: 16
},
end: LineColumn {
line: 3,
column: 20
}
}
))
)],
);
}
#[test]
fn test_conditional_okay() {
let contents = 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 = 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 {
start: LineColumn {
line: 4,
column: 28
},
end: LineColumn {
line: 4,
column: 47
}
},
Span {
start: LineColumn {
line: 4,
column: 28
},
end: LineColumn {
line: 4,
column: 37
}
},
"use_state".to_string()
),
ClosureInfo::new(Span {
start: LineColumn {
line: 3,
column: 25
},
end: LineColumn {
line: 6,
column: 17
}
})
)]
);
}
#[test]
fn test_hook_outside_component() {
let contents = 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 {
start: LineColumn {
line: 3,
column: 25
},
end: LineColumn {
line: 3,
column: 44
}
},
Span {
start: LineColumn {
line: 3,
column: 25
},
end: LineColumn {
line: 3,
column: 34
}
},
"use_state".to_string()
))]
);
}
#[test]
fn test_hook_inside_hook() {
let contents = 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![]);
}
}

View file

@ -0,0 +1,181 @@
#[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` keyword only.
pub keyword_span: Span,
}
impl IfInfo {
pub const fn new(span: Span, keyword_span: Span) -> Self {
Self { span, keyword_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` keyword only.
pub keyword_span: Span,
}
impl MatchInfo {
pub const fn new(span: Span, keyword_span: Span) -> Self {
Self { span, keyword_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 keyword_span: Span,
}
impl ForInfo {
pub const fn new(span: Span, keyword_span: Span) -> Self {
Self { span, keyword_span }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// Information about a `while` loop.
pub struct WhileInfo {
pub span: Span,
pub keyword_span: Span,
}
impl WhileInfo {
pub const fn new(span: Span, keyword_span: Span) -> Self {
Self { span, keyword_span }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// Information about a `loop` loop.
pub struct LoopInfo {
pub span: Span,
pub keyword_span: Span,
}
impl LoopInfo {
pub const fn new(span: Span, keyword_span: Span) -> Self {
Self { span, keyword_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 start: LineColumn,
pub end: LineColumn,
}
impl From<proc_macro2::Span> for Span {
fn from(span: proc_macro2::Span) -> Self {
Self {
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,
}
}
}

View file

@ -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"] }

View 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);
}
}
}
}

View file

@ -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"),

View file

@ -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);