diff --git a/packages/check/Cargo.toml b/packages/check/Cargo.toml index 9757eaa45..701d845b3 100644 --- a/packages/check/Cargo.toml +++ b/packages/check/Cargo.toml @@ -19,7 +19,8 @@ serde = { version = "1.0.136", features = ["derive"] } prettyplease = { package = "prettier-please", version = "0.1.16", features = [ "verbatim", ] } -owo-colors = "3.5.0" +owo-colors = { version = "3.5.0", features = ["supports-colors"] } [dev-dependencies] +indoc = "2.0.3" pretty_assertions = "1.2.1" diff --git a/packages/check/src/check.rs b/packages/check/src/check.rs index 7d89bf0a3..b8298689b 100644 --- a/packages/check/src/check.rs +++ b/packages/check/src/check.rs @@ -29,11 +29,12 @@ 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 { + IssueReport::new( path, - file_content: file_content.to_string(), - issues: visit_hooks.issues, - } + std::env::current_dir().unwrap_or_default(), + file_content.to_string(), + visit_hooks.issues, + ) } #[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) { self.context.push(Node::If(IfInfo::new( 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); 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) { self.context.push(Node::Match(MatchInfo::new( 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); 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) { self.context.push(Node::For(ForInfo::new( 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); 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) { self.context.push(Node::While(WhileInfo::new( 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); 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(), - ))); + self.context + .push(Node::Loop(LoopInfo::new(i.span().into()))); syn::visit::visit_expr_loop(self, i); self.context.pop(); } @@ -224,3 +239,328 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks { 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![]); + } +} diff --git a/packages/check/src/issues.rs b/packages/check/src/issues.rs index 30357aa03..9893cde25 100644 --- a/packages/check/src/issues.rs +++ b/packages/check/src/issues.rs @@ -1,62 +1,88 @@ use owo_colors::{ colors::{css::LightBlue, BrightRed}, - OwoColorize, + OwoColorize, Stream, }; use std::{ fmt::Display, 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. pub struct IssueReport { pub path: PathBuf, + pub crate_root: PathBuf, pub file_content: String, pub issues: Vec, } impl IssueReport { - pub fn new(path: PathBuf, file_content: S, issues: Vec) -> Self { + pub fn new( + path: PathBuf, + crate_root: PathBuf, + file_content: S, + issues: Vec, + ) -> Self { Self { path, + crate_root, file_content: file_content.to_string(), issues, } } } +fn lightblue(text: &str) -> String { + text.if_supports_color(Stream::Stderr, |text| text.fg::()) + .to_string() +} + +fn brightred(text: &str) -> String { + text.if_supports_color(Stream::Stderr, |text| text.fg::()) + .to_string() +} + +fn bold(text: &str) -> String { + text.if_supports_color(Stream::Stderr, |text| text.bold()) + .to_string() +} + impl Display for IssueReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let relative_file = Path::new(&self.path) - .strip_prefix(std::env::current_dir().unwrap()) + .strip_prefix(&self.crate_root) .unwrap_or(Path::new(&self.path)) .display(); + let pipe_char = lightblue("|"); + for (i, issue) in self.issues.iter().enumerate() { let hook_info = issue.hook_info(); let hook_span = hook_info.span; let hook_name_span = hook_info.name_span; - let error_line = format!("{}: {}", "error".fg::(), issue); - writeln!(f, "{}", error_line.bold())?; + let error_line = format!("{}: {}", brightred("error"), issue); + writeln!(f, "{}", bold(&error_line))?; 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::())?; + writeln!(f, "{:>max_line_num_len$} {}", "", pipe_char)?; for (i, line) in self.file_content.lines().enumerate() { let line_num = i + 1; if line_num >= hook_span.start.line && line_num <= hook_span.end.line { writeln!( f, "{:>max_line_num_len$} {} {}", - line_num.fg::(), - "|".fg::(), + lightblue(&line_num.to_string()), + pipe_char, line, )?; if line_num == hook_span.start.line { @@ -71,13 +97,54 @@ impl Display for IssueReport { f, "{:>max_line_num_len$} {} {}", "", - "|".fg::(), - caret.fg::(), + pipe_char, + brightred(&caret), )?; } } } + let note_text_prefix = format!( + "{:>max_line_num_len$} {}\n{:>max_line_num_len$} {} note:", + "", + pipe_char, + "", + lightblue("=") + ); + + match issue { + Issue::HookInsideConditional( + _, + ConditionalInfo::If(IfInfo { span: _, head_span }), + ) + | Issue::HookInsideConditional( + _, + ConditionalInfo::Match(MatchInfo { span: _, head_span }), + ) => { + if let Some(source_text) = &head_span.source_text { + writeln!( + f, + "{} `{} {{ … }}` is the conditional", + note_text_prefix, source_text, + )?; + } + } + Issue::HookInsideLoop(_, AnyLoopInfo::For(ForInfo { span: _, head_span })) + | Issue::HookInsideLoop(_, AnyLoopInfo::While(WhileInfo { span: _, head_span })) => { + if let Some(source_text) = &head_span.source_text { + writeln!( + f, + "{} `{} {{ … }}` is the loop", + note_text_prefix, source_text, + )?; + } + } + Issue::HookInsideLoop(_, AnyLoopInfo::Loop(_)) => { + writeln!(f, "{} `loop {{ … }}` is the loop", note_text_prefix,)?; + } + Issue::HookOutsideComponent(_) | Issue::HookInsideClosure(_, _) => {} + } + if i < self.issues.len() - 1 { writeln!(f)?; } @@ -138,7 +205,7 @@ impl std::fmt::Display for Issue { ) } 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) => { 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()); + } +} diff --git a/packages/check/src/lib.rs b/packages/check/src/lib.rs index 712462e40..3870fb610 100644 --- a/packages/check/src/lib.rs +++ b/packages/check/src/lib.rs @@ -4,476 +4,3 @@ 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![]); - } -} diff --git a/packages/check/src/metadata.rs b/packages/check/src/metadata.rs index d886dcf32..28ab87b74 100644 --- a/packages/check/src/metadata.rs +++ b/packages/check/src/metadata.rs @@ -29,13 +29,13 @@ pub enum ConditionalInfo { 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, + /// The span of the `if true` part only. + pub head_span: Span, } impl IfInfo { - pub const fn new(span: Span, keyword_span: Span) -> Self { - Self { span, keyword_span } + pub const fn new(span: Span, head_span: Span) -> Self { + Self { span, head_span } } } @@ -43,13 +43,13 @@ impl IfInfo { 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, + /// The span of the `match true` part only. + pub head_span: Span, } impl MatchInfo { - pub const fn new(span: Span, keyword_span: Span) -> Self { - Self { span, keyword_span } + pub const fn new(span: Span, head_span: Span) -> Self { + Self { span, head_span } } } @@ -65,12 +65,12 @@ pub enum AnyLoopInfo { /// Information about a `for` loop. pub struct ForInfo { pub span: Span, - pub keyword_span: Span, + pub head_span: Span, } impl ForInfo { - pub const fn new(span: Span, keyword_span: Span) -> Self { - Self { span, keyword_span } + pub const fn new(span: Span, head_span: Span) -> Self { + Self { span, head_span } } } @@ -78,12 +78,12 @@ impl ForInfo { /// Information about a `while` loop. pub struct WhileInfo { pub span: Span, - pub keyword_span: Span, + pub head_span: Span, } impl WhileInfo { - pub const fn new(span: Span, keyword_span: Span) -> Self { - Self { span, keyword_span } + pub const fn new(span: Span, head_span: Span) -> Self { + Self { span, head_span } } } @@ -91,12 +91,11 @@ impl WhileInfo { /// 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 } + pub const fn new(span: Span) -> Self { + Self { span } } } @@ -151,13 +150,35 @@ impl FnInfo { #[derive(Debug, Clone, PartialEq, Eq)] /// A span of text in a source code file. pub struct Span { + pub source_text: Option, pub start: LineColumn, pub end: LineColumn, } +impl Span { + pub fn new_from_str(source_text: &str, start: LineColumn) -> Self { + let mut lines = source_text.lines(); + let first_line = lines.next().unwrap_or_default(); + let mut end = LineColumn { + line: start.line, + column: start.column + first_line.len(), + }; + for line in lines { + end.line += 1; + end.column = line.len(); + } + Self { + source_text: Some(source_text.to_string()), + start, + end, + } + } +} + impl From for Span { fn from(span: proc_macro2::Span) -> Self { Self { + source_text: span.source_text(), start: span.start().into(), end: span.end().into(), }