improve test coverage and display output
This commit is contained in:
5 files changed
+680
-516
No files matched your search
@@ -19,7 +19,8 @@ serde = { version = "1.0.136", features = ["derive"] }
|
|||||||
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
|
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
|
||||||
"verbatim",
|
"verbatim",
|
||||||
] }
|
] }
|
||||||
owo-colors = "3.5.0"
|
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
indoc = "2.0.3"
|
||||||
pretty_assertions = "1.2.1"
|
pretty_assertions = "1.2.1"
|
||||||
+352
-12
@@ -29,11 +29,12 @@ pub fn check_file(path: PathBuf, file_content: &str) -> IssueReport {
|
|||||||
let file = syn::parse_file(file_content).unwrap();
|
let file = syn::parse_file(file_content).unwrap();
|
||||||
let mut visit_hooks = VisitHooks::new();
|
let mut visit_hooks = VisitHooks::new();
|
||||||
visit_hooks.visit_file(&file);
|
visit_hooks.visit_file(&file);
|
||||||
IssueReport {
|
IssueReport::new(
|
||||||
path,
|
path,
|
||||||
file_content: file_content.to_string(),
|
std::env::current_dir().unwrap_or_default(),
|
||||||
issues: visit_hooks.issues,
|
file_content.to_string(),
|
||||||
}
|
visit_hooks.issues,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -175,7 +176,11 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
|||||||
fn visit_expr_if(&mut self, i: &'ast syn::ExprIf) {
|
fn visit_expr_if(&mut self, i: &'ast syn::ExprIf) {
|
||||||
self.context.push(Node::If(IfInfo::new(
|
self.context.push(Node::If(IfInfo::new(
|
||||||
i.span().into(),
|
i.span().into(),
|
||||||
i.if_token.span().into(),
|
i.if_token
|
||||||
|
.span()
|
||||||
|
.join(i.cond.span())
|
||||||
|
.unwrap_or_else(|| i.span())
|
||||||
|
.into(),
|
||||||
)));
|
)));
|
||||||
syn::visit::visit_expr_if(self, i);
|
syn::visit::visit_expr_if(self, i);
|
||||||
self.context.pop();
|
self.context.pop();
|
||||||
@@ -184,7 +189,11 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
|||||||
fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
|
fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
|
||||||
self.context.push(Node::Match(MatchInfo::new(
|
self.context.push(Node::Match(MatchInfo::new(
|
||||||
i.span().into(),
|
i.span().into(),
|
||||||
i.match_token.span().into(),
|
i.match_token
|
||||||
|
.span()
|
||||||
|
.join(i.expr.span())
|
||||||
|
.unwrap_or_else(|| i.span())
|
||||||
|
.into(),
|
||||||
)));
|
)));
|
||||||
syn::visit::visit_expr_match(self, i);
|
syn::visit::visit_expr_match(self, i);
|
||||||
self.context.pop();
|
self.context.pop();
|
||||||
@@ -193,7 +202,11 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
|||||||
fn visit_expr_for_loop(&mut self, i: &'ast syn::ExprForLoop) {
|
fn visit_expr_for_loop(&mut self, i: &'ast syn::ExprForLoop) {
|
||||||
self.context.push(Node::For(ForInfo::new(
|
self.context.push(Node::For(ForInfo::new(
|
||||||
i.span().into(),
|
i.span().into(),
|
||||||
i.for_token.span().into(),
|
i.for_token
|
||||||
|
.span()
|
||||||
|
.join(i.expr.span())
|
||||||
|
.unwrap_or_else(|| i.span())
|
||||||
|
.into(),
|
||||||
)));
|
)));
|
||||||
syn::visit::visit_expr_for_loop(self, i);
|
syn::visit::visit_expr_for_loop(self, i);
|
||||||
self.context.pop();
|
self.context.pop();
|
||||||
@@ -202,17 +215,19 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
|||||||
fn visit_expr_while(&mut self, i: &'ast syn::ExprWhile) {
|
fn visit_expr_while(&mut self, i: &'ast syn::ExprWhile) {
|
||||||
self.context.push(Node::While(WhileInfo::new(
|
self.context.push(Node::While(WhileInfo::new(
|
||||||
i.span().into(),
|
i.span().into(),
|
||||||
i.while_token.span().into(),
|
i.while_token
|
||||||
|
.span()
|
||||||
|
.join(i.cond.span())
|
||||||
|
.unwrap_or_else(|| i.span())
|
||||||
|
.into(),
|
||||||
)));
|
)));
|
||||||
syn::visit::visit_expr_while(self, i);
|
syn::visit::visit_expr_while(self, i);
|
||||||
self.context.pop();
|
self.context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_expr_loop(&mut self, i: &'ast syn::ExprLoop) {
|
fn visit_expr_loop(&mut self, i: &'ast syn::ExprLoop) {
|
||||||
self.context.push(Node::Loop(LoopInfo::new(
|
self.context
|
||||||
i.span().into(),
|
.push(Node::Loop(LoopInfo::new(i.span().into())));
|
||||||
i.loop_token.span().into(),
|
|
||||||
)));
|
|
||||||
syn::visit::visit_expr_loop(self, i);
|
syn::visit::visit_expr_loop(self, i);
|
||||||
self.context.pop();
|
self.context.pop();
|
||||||
}
|
}
|
||||||
@@ -224,3 +239,328 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
|||||||
self.context.pop();
|
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_issues() {
|
||||||
|
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_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![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+288
-13
@@ -1,62 +1,88 @@
|
|||||||
use owo_colors::{
|
use owo_colors::{
|
||||||
colors::{css::LightBlue, BrightRed},
|
colors::{css::LightBlue, BrightRed},
|
||||||
OwoColorize,
|
OwoColorize, Stream,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::metadata::{AnyLoopInfo, ClosureInfo, ConditionalInfo, HookInfo};
|
use crate::metadata::{
|
||||||
|
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo, WhileInfo,
|
||||||
|
};
|
||||||
|
|
||||||
/// The result of checking a Dioxus file for issues.
|
/// The result of checking a Dioxus file for issues.
|
||||||
pub struct IssueReport {
|
pub struct IssueReport {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub crate_root: PathBuf,
|
||||||
pub file_content: String,
|
pub file_content: String,
|
||||||
pub issues: Vec<Issue>,
|
pub issues: Vec<Issue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IssueReport {
|
impl IssueReport {
|
||||||
pub fn new<S: ToString>(path: PathBuf, file_content: S, issues: Vec<Issue>) -> Self {
|
pub fn new<S: ToString>(
|
||||||
|
path: PathBuf,
|
||||||
|
crate_root: PathBuf,
|
||||||
|
file_content: S,
|
||||||
|
issues: Vec<Issue>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
|
crate_root,
|
||||||
file_content: file_content.to_string(),
|
file_content: file_content.to_string(),
|
||||||
issues,
|
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 {
|
impl Display for IssueReport {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let relative_file = Path::new(&self.path)
|
let relative_file = Path::new(&self.path)
|
||||||
.strip_prefix(std::env::current_dir().unwrap())
|
.strip_prefix(&self.crate_root)
|
||||||
.unwrap_or(Path::new(&self.path))
|
.unwrap_or(Path::new(&self.path))
|
||||||
.display();
|
.display();
|
||||||
|
|
||||||
|
let pipe_char = lightblue("|");
|
||||||
|
|
||||||
for (i, issue) in self.issues.iter().enumerate() {
|
for (i, issue) in self.issues.iter().enumerate() {
|
||||||
let hook_info = issue.hook_info();
|
let hook_info = issue.hook_info();
|
||||||
let hook_span = hook_info.span;
|
let hook_span = hook_info.span;
|
||||||
let hook_name_span = hook_info.name_span;
|
let hook_name_span = hook_info.name_span;
|
||||||
let error_line = format!("{}: {}", "error".fg::<BrightRed>(), issue);
|
let error_line = format!("{}: {}", brightred("error"), issue);
|
||||||
writeln!(f, "{}", error_line.bold())?;
|
writeln!(f, "{}", bold(&error_line))?;
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
" {} {}:{}:{}",
|
" {} {}:{}:{}",
|
||||||
"-->".fg::<LightBlue>(),
|
lightblue("-->"),
|
||||||
relative_file,
|
relative_file,
|
||||||
hook_span.start.line,
|
hook_span.start.line,
|
||||||
hook_span.start.column + 1
|
hook_span.start.column + 1
|
||||||
)?;
|
)?;
|
||||||
let max_line_num_len = hook_span.end.line.to_string().len();
|
let max_line_num_len = hook_span.end.line.to_string().len();
|
||||||
writeln!(f, "{:>max_line_num_len$} {}", "", "|".fg::<LightBlue>())?;
|
writeln!(f, "{:>max_line_num_len$} {}", "", pipe_char)?;
|
||||||
for (i, line) in self.file_content.lines().enumerate() {
|
for (i, line) in self.file_content.lines().enumerate() {
|
||||||
let line_num = i + 1;
|
let line_num = i + 1;
|
||||||
if line_num >= hook_span.start.line && line_num <= hook_span.end.line {
|
if line_num >= hook_span.start.line && line_num <= hook_span.end.line {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
"{:>max_line_num_len$} {} {}",
|
"{:>max_line_num_len$} {} {}",
|
||||||
line_num.fg::<LightBlue>(),
|
lightblue(&line_num.to_string()),
|
||||||
"|".fg::<LightBlue>(),
|
pipe_char,
|
||||||
line,
|
line,
|
||||||
)?;
|
)?;
|
||||||
if line_num == hook_span.start.line {
|
if line_num == hook_span.start.line {
|
||||||
@@ -71,13 +97,54 @@ impl Display for IssueReport {
|
|||||||
f,
|
f,
|
||||||
"{:>max_line_num_len$} {} {}",
|
"{:>max_line_num_len$} {} {}",
|
||||||
"",
|
"",
|
||||||
"|".fg::<LightBlue>(),
|
pipe_char,
|
||||||
caret.fg::<BrightRed>(),
|
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 {
|
if i < self.issues.len() - 1 {
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
}
|
}
|
||||||
@@ -138,7 +205,7 @@ impl std::fmt::Display for Issue {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Issue::HookInsideClosure(hook_info, _) => {
|
Issue::HookInsideClosure(hook_info, _) => {
|
||||||
write!(f, "hook called inside closure: `{}`", hook_info.name)
|
write!(f, "hook called in a closure: `{}`", hook_info.name)
|
||||||
}
|
}
|
||||||
Issue::HookOutsideComponent(hook_info) => {
|
Issue::HookOutsideComponent(hook_info) => {
|
||||||
write!(
|
write!(
|
||||||
@@ -150,3 +217,211 @@ impl std::fmt::Display for Issue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,476 +4,3 @@ mod metadata;
|
|||||||
|
|
||||||
pub use check::check_file;
|
pub use check::check_file;
|
||||||
pub use issues::{Issue, IssueReport};
|
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![]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,13 +29,13 @@ pub enum ConditionalInfo {
|
|||||||
pub struct IfInfo {
|
pub struct IfInfo {
|
||||||
/// The span of the `if` statement, e.g. `if true { ... }`.
|
/// The span of the `if` statement, e.g. `if true { ... }`.
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
/// The span of the `if` keyword only.
|
/// The span of the `if true` part only.
|
||||||
pub keyword_span: Span,
|
pub head_span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IfInfo {
|
impl IfInfo {
|
||||||
pub const fn new(span: Span, keyword_span: Span) -> Self {
|
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||||
Self { span, keyword_span }
|
Self { span, head_span }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +43,13 @@ impl IfInfo {
|
|||||||
pub struct MatchInfo {
|
pub struct MatchInfo {
|
||||||
/// The span of the `match` statement, e.g. `match true { ... }`.
|
/// The span of the `match` statement, e.g. `match true { ... }`.
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
/// The span of the `match` keyword only.
|
/// The span of the `match true` part only.
|
||||||
pub keyword_span: Span,
|
pub head_span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MatchInfo {
|
impl MatchInfo {
|
||||||
pub const fn new(span: Span, keyword_span: Span) -> Self {
|
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||||
Self { span, keyword_span }
|
Self { span, head_span }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +65,12 @@ pub enum AnyLoopInfo {
|
|||||||
/// Information about a `for` loop.
|
/// Information about a `for` loop.
|
||||||
pub struct ForInfo {
|
pub struct ForInfo {
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub keyword_span: Span,
|
pub head_span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForInfo {
|
impl ForInfo {
|
||||||
pub const fn new(span: Span, keyword_span: Span) -> Self {
|
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||||
Self { span, keyword_span }
|
Self { span, head_span }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,12 +78,12 @@ impl ForInfo {
|
|||||||
/// Information about a `while` loop.
|
/// Information about a `while` loop.
|
||||||
pub struct WhileInfo {
|
pub struct WhileInfo {
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub keyword_span: Span,
|
pub head_span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WhileInfo {
|
impl WhileInfo {
|
||||||
pub const fn new(span: Span, keyword_span: Span) -> Self {
|
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||||
Self { span, keyword_span }
|
Self { span, head_span }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +91,11 @@ impl WhileInfo {
|
|||||||
/// Information about a `loop` loop.
|
/// Information about a `loop` loop.
|
||||||
pub struct LoopInfo {
|
pub struct LoopInfo {
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub keyword_span: Span,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoopInfo {
|
impl LoopInfo {
|
||||||
pub const fn new(span: Span, keyword_span: Span) -> Self {
|
pub const fn new(span: Span) -> Self {
|
||||||
Self { span, keyword_span }
|
Self { span }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,13 +150,35 @@ impl FnInfo {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// A span of text in a source code file.
|
/// A span of text in a source code file.
|
||||||
pub struct Span {
|
pub struct Span {
|
||||||
|
pub source_text: Option<String>,
|
||||||
pub start: LineColumn,
|
pub start: LineColumn,
|
||||||
pub end: 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 {
|
impl From<proc_macro2::Span> for Span {
|
||||||
fn from(span: proc_macro2::Span) -> Self {
|
fn from(span: proc_macro2::Span) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
source_text: span.source_text(),
|
||||||
start: span.start().into(),
|
start: span.start().into(),
|
||||||
end: span.end().into(),
|
end: span.end().into(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user