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:
11 files changed
+1255
No files matched your search
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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![]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user