Przeglądaj źródła

improve test coverage and display output

Brian Donovan 1 rok temu
rodzic
commit
1531893a45

+ 2 - 1
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"

+ 352 - 12
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![]);
+    }
+}

+ 288 - 13
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<Issue>,
 }
 
 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 {
             path,
+            crate_root,
             file_content: file_content.to_string(),
             issues,
         }
     }
 }
 
+fn lightblue(text: &str) -> String {
+    text.if_supports_color(Stream::Stderr, |text| text.fg::<LightBlue>())
+        .to_string()
+}
+
+fn brightred(text: &str) -> String {
+    text.if_supports_color(Stream::Stderr, |text| text.fg::<BrightRed>())
+        .to_string()
+}
+
+fn bold(text: &str) -> String {
+    text.if_supports_color(Stream::Stderr, |text| text.bold())
+        .to_string()
+}
+
 impl Display for IssueReport {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let relative_file = Path::new(&self.path)
-            .strip_prefix(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::<BrightRed>(), issue);
-            writeln!(f, "{}", error_line.bold())?;
+            let error_line = format!("{}: {}", brightred("error"), issue);
+            writeln!(f, "{}", bold(&error_line))?;
             writeln!(
                 f,
                 "  {} {}:{}:{}",
-                "-->".fg::<LightBlue>(),
+                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>())?;
+            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::<LightBlue>(),
-                        "|".fg::<LightBlue>(),
+                        lightblue(&line_num.to_string()),
+                        pipe_char,
                         line,
                     )?;
                     if line_num == hook_span.start.line {
@@ -71,11 +97,52 @@ impl Display for IssueReport {
                             f,
                             "{:>max_line_num_len$} {} {}",
                             "",
-                            "|".fg::<LightBlue>(),
-                            caret.fg::<BrightRed>(),
+                            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 {
@@ -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());
+    }
+}

+ 0 - 473
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![]);
-    }
-}

+ 38 - 17
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<String>,
     pub start: LineColumn,
     pub end: LineColumn,
 }
 
+impl Span {
+    pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
+        let mut lines = source_text.lines();
+        let first_line = lines.next().unwrap_or_default();
+        let mut end = LineColumn {
+            line: start.line,
+            column: start.column + first_line.len(),
+        };
+        for line in lines {
+            end.line += 1;
+            end.column = line.len();
+        }
+        Self {
+            source_text: Some(source_text.to_string()),
+            start,
+            end,
+        }
+    }
+}
+
 impl From<proc_macro2::Span> for Span {
     fn from(span: proc_macro2::Span) -> Self {
         Self {
+            source_text: span.source_text(),
             start: span.start().into(),
             end: span.end().into(),
         }