dioxus_check/
issues.rs

1use owo_colors::{
2    colors::{css::LightBlue, BrightRed},
3    OwoColorize, Stream,
4};
5use std::{
6    fmt::Display,
7    path::{Path, PathBuf},
8};
9
10use crate::metadata::{
11    AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo,
12    WhileInfo,
13};
14
15/// The result of checking a Dioxus file for issues.
16pub struct IssueReport {
17    pub path: PathBuf,
18    pub crate_root: PathBuf,
19    pub file_content: String,
20    pub issues: Vec<Issue>,
21}
22
23impl IssueReport {
24    pub fn new<S: ToString>(
25        path: PathBuf,
26        crate_root: PathBuf,
27        file_content: S,
28        issues: Vec<Issue>,
29    ) -> Self {
30        Self {
31            path,
32            crate_root,
33            file_content: file_content.to_string(),
34            issues,
35        }
36    }
37}
38
39fn lightblue(text: &str) -> String {
40    text.if_supports_color(Stream::Stderr, |text| text.fg::<LightBlue>())
41        .to_string()
42}
43
44fn brightred(text: &str) -> String {
45    text.if_supports_color(Stream::Stderr, |text| text.fg::<BrightRed>())
46        .to_string()
47}
48
49fn bold(text: &str) -> String {
50    text.if_supports_color(Stream::Stderr, |text| text.bold())
51        .to_string()
52}
53
54impl Display for IssueReport {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        let relative_file = Path::new(&self.path)
57            .strip_prefix(&self.crate_root)
58            .unwrap_or(Path::new(&self.path))
59            .display();
60
61        let pipe_char = lightblue("|");
62
63        for (i, issue) in self.issues.iter().enumerate() {
64            let hook_info = issue.hook_info();
65            let hook_span = hook_info.span;
66            let hook_name_span = hook_info.name_span;
67            let error_line = format!("{}: {}", brightred("error"), issue);
68            writeln!(f, "{}", bold(&error_line))?;
69            writeln!(
70                f,
71                "  {} {}:{}:{}",
72                lightblue("-->"),
73                relative_file,
74                hook_span.start.line,
75                hook_span.start.column + 1
76            )?;
77            let max_line_num_len = hook_span.end.line.to_string().len();
78            writeln!(f, "{:>max_line_num_len$} {}", "", pipe_char)?;
79            for (i, line) in self.file_content.lines().enumerate() {
80                let line_num = i + 1;
81                if line_num >= hook_span.start.line && line_num <= hook_span.end.line {
82                    writeln!(
83                        f,
84                        "{:>max_line_num_len$} {} {}",
85                        lightblue(&line_num.to_string()),
86                        pipe_char,
87                        line,
88                    )?;
89                    if line_num == hook_span.start.line {
90                        let mut caret = String::new();
91                        for _ in 0..hook_name_span.start.column {
92                            caret.push(' ');
93                        }
94                        for _ in hook_name_span.start.column..hook_name_span.end.column {
95                            caret.push('^');
96                        }
97                        writeln!(
98                            f,
99                            "{:>max_line_num_len$} {} {}",
100                            "",
101                            pipe_char,
102                            brightred(&caret),
103                        )?;
104                    }
105                }
106            }
107
108            let note_text_prefix = format!(
109                "{:>max_line_num_len$} {}\n{:>max_line_num_len$} {} note:",
110                "",
111                pipe_char,
112                "",
113                lightblue("=")
114            );
115
116            match issue {
117                Issue::HookInsideConditional(
118                    _,
119                    ConditionalInfo::If(IfInfo { span: _, head_span }),
120                )
121                | Issue::HookInsideConditional(
122                    _,
123                    ConditionalInfo::Match(MatchInfo { span: _, head_span }),
124                ) => {
125                    if let Some(source_text) = &head_span.source_text {
126                        writeln!(
127                            f,
128                            "{} `{} {{ … }}` is the conditional",
129                            note_text_prefix, source_text,
130                        )?;
131                    }
132                }
133                Issue::HookInsideLoop(_, AnyLoopInfo::For(ForInfo { span: _, head_span }))
134                | Issue::HookInsideLoop(_, AnyLoopInfo::While(WhileInfo { span: _, head_span })) => {
135                    if let Some(source_text) = &head_span.source_text {
136                        writeln!(
137                            f,
138                            "{} `{} {{ … }}` is the loop",
139                            note_text_prefix, source_text,
140                        )?;
141                    }
142                }
143                Issue::HookInsideLoop(_, AnyLoopInfo::Loop(_)) => {
144                    writeln!(f, "{} `loop {{ … }}` is the loop", note_text_prefix,)?;
145                }
146                Issue::HookOutsideComponent(_)
147                | Issue::HookInsideClosure(_, _)
148                | Issue::HookInsideAsync(_, _) => {}
149            }
150
151            if i < self.issues.len() - 1 {
152                writeln!(f)?;
153            }
154        }
155
156        Ok(())
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161#[non_exhaustive]
162#[allow(clippy::enum_variant_names)] // we'll add non-hook ones in the future
163/// Issues that might be found via static analysis of a Dioxus file.
164pub enum Issue {
165    /// <https://dioxuslabs.com/learn/0.6/reference/hooks#no-hooks-in-conditionals>
166    HookInsideConditional(HookInfo, ConditionalInfo),
167    /// <https://dioxuslabs.com/learn/0.6/reference/hooks#no-hooks-in-loops>
168    HookInsideLoop(HookInfo, AnyLoopInfo),
169    /// <https://dioxuslabs.com/learn/0.6/reference/hooks#no-hooks-in-closures>
170    HookInsideClosure(HookInfo, ClosureInfo),
171    HookInsideAsync(HookInfo, AsyncInfo),
172    HookOutsideComponent(HookInfo),
173}
174
175impl Issue {
176    pub fn hook_info(&self) -> HookInfo {
177        match self {
178            Issue::HookInsideConditional(hook_info, _)
179            | Issue::HookInsideLoop(hook_info, _)
180            | Issue::HookInsideClosure(hook_info, _)
181            | Issue::HookInsideAsync(hook_info, _)
182            | Issue::HookOutsideComponent(hook_info) => hook_info.clone(),
183        }
184    }
185}
186
187impl std::fmt::Display for Issue {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            Issue::HookInsideConditional(hook_info, conditional_info) => {
191                write!(
192                    f,
193                    "hook called conditionally: `{}` (inside `{}`)",
194                    hook_info.name,
195                    match conditional_info {
196                        ConditionalInfo::If(_) => "if",
197                        ConditionalInfo::Match(_) => "match",
198                    }
199                )
200            }
201            Issue::HookInsideLoop(hook_info, loop_info) => {
202                write!(
203                    f,
204                    "hook called in a loop: `{}` (inside {})",
205                    hook_info.name,
206                    match loop_info {
207                        AnyLoopInfo::For(_) => "`for` loop",
208                        AnyLoopInfo::While(_) => "`while` loop",
209                        AnyLoopInfo::Loop(_) => "`loop`",
210                    }
211                )
212            }
213            Issue::HookInsideClosure(hook_info, _) => {
214                write!(f, "hook called in a closure: `{}`", hook_info.name)
215            }
216            Issue::HookInsideAsync(hook_info, _) => {
217                write!(f, "hook called in an async block: `{}`", hook_info.name)
218            }
219            Issue::HookOutsideComponent(hook_info) => {
220                write!(
221                    f,
222                    "hook called outside component or hook: `{}`",
223                    hook_info.name
224                )
225            }
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use crate::check_file;
233    use indoc::indoc;
234    use pretty_assertions::assert_eq;
235
236    #[test]
237    fn test_issue_report_display_conditional_if() {
238        owo_colors::set_override(false);
239        let issue_report = check_file(
240            "src/main.rs".into(),
241            indoc! {r#"
242                fn App() -> Element {
243                    if you_are_happy && you_know_it {
244                        let something = use_signal(|| "hands");
245                        println!("clap your {something}")
246                    }
247                }
248            "#},
249        );
250
251        let expected = indoc! {r#"
252            error: hook called conditionally: `use_signal` (inside `if`)
253              --> src/main.rs:3:25
254              |
255            3 |         let something = use_signal(|| "hands");
256              |                         ^^^^^^^^^^
257              |
258              = note: `if you_are_happy && you_know_it { … }` is the conditional
259        "#};
260
261        assert_eq!(expected, issue_report.to_string());
262    }
263
264    #[test]
265    fn test_issue_report_display_conditional_match() {
266        owo_colors::set_override(false);
267        let issue_report = check_file(
268            "src/main.rs".into(),
269            indoc! {r#"
270                fn App() -> Element {
271                    match you_are_happy && you_know_it {
272                        true => {
273                            let something = use_signal(|| "hands");
274                            println!("clap your {something}")
275                        }
276                        _ => {}
277                    }
278                }
279            "#},
280        );
281
282        let expected = indoc! {r#"
283            error: hook called conditionally: `use_signal` (inside `match`)
284              --> src/main.rs:4:29
285              |
286            4 |             let something = use_signal(|| "hands");
287              |                             ^^^^^^^^^^
288              |
289              = note: `match you_are_happy && you_know_it { … }` is the conditional
290        "#};
291
292        assert_eq!(expected, issue_report.to_string());
293    }
294
295    #[test]
296    fn test_issue_report_display_for_loop() {
297        owo_colors::set_override(false);
298        let issue_report = check_file(
299            "src/main.rs".into(),
300            indoc! {r#"
301                fn App() -> Element {
302                    for i in 0..10 {
303                        let something = use_signal(|| "hands");
304                        println!("clap your {something}")
305                    }
306                }
307            "#},
308        );
309
310        let expected = indoc! {r#"
311            error: hook called in a loop: `use_signal` (inside `for` loop)
312              --> src/main.rs:3:25
313              |
314            3 |         let something = use_signal(|| "hands");
315              |                         ^^^^^^^^^^
316              |
317              = note: `for i in 0..10 { … }` is the loop
318        "#};
319
320        assert_eq!(expected, issue_report.to_string());
321    }
322
323    #[test]
324    fn test_issue_report_display_while_loop() {
325        owo_colors::set_override(false);
326        let issue_report = check_file(
327            "src/main.rs".into(),
328            indoc! {r#"
329                fn App() -> Element {
330                    while check_thing() {
331                        let something = use_signal(|| "hands");
332                        println!("clap your {something}")
333                    }
334                }
335            "#},
336        );
337
338        let expected = indoc! {r#"
339            error: hook called in a loop: `use_signal` (inside `while` loop)
340              --> src/main.rs:3:25
341              |
342            3 |         let something = use_signal(|| "hands");
343              |                         ^^^^^^^^^^
344              |
345              = note: `while check_thing() { … }` is the loop
346        "#};
347
348        assert_eq!(expected, issue_report.to_string());
349    }
350
351    #[test]
352    fn test_issue_report_display_loop() {
353        owo_colors::set_override(false);
354        let issue_report = check_file(
355            "src/main.rs".into(),
356            indoc! {r#"
357                fn App() -> Element {
358                    loop {
359                        let something = use_signal(|| "hands");
360                        println!("clap your {something}")
361                    }
362                }
363            "#},
364        );
365
366        let expected = indoc! {r#"
367            error: hook called in a loop: `use_signal` (inside `loop`)
368              --> src/main.rs:3:25
369              |
370            3 |         let something = use_signal(|| "hands");
371              |                         ^^^^^^^^^^
372              |
373              = note: `loop { … }` is the loop
374        "#};
375
376        assert_eq!(expected, issue_report.to_string());
377    }
378
379    #[test]
380    fn test_issue_report_display_closure() {
381        owo_colors::set_override(false);
382        let issue_report = check_file(
383            "src/main.rs".into(),
384            indoc! {r#"
385                fn App() -> Element {
386                    let something = || {
387                        let something = use_signal(|| "hands");
388                        println!("clap your {something}")
389                    };
390                }
391            "#},
392        );
393
394        let expected = indoc! {r#"
395            error: hook called in a closure: `use_signal`
396              --> src/main.rs:3:25
397              |
398            3 |         let something = use_signal(|| "hands");
399              |                         ^^^^^^^^^^
400        "#};
401
402        assert_eq!(expected, issue_report.to_string());
403    }
404
405    #[test]
406    fn test_issue_report_display_multiline_hook() {
407        owo_colors::set_override(false);
408        let issue_report = check_file(
409            "src/main.rs".into(),
410            indoc! {r#"
411                fn App() -> Element {
412                    if you_are_happy && you_know_it {
413                        let something = use_signal(|| {
414                            "hands"
415                        });
416                        println!("clap your {something}")
417                    }
418                }
419            "#},
420        );
421
422        let expected = indoc! {r#"
423            error: hook called conditionally: `use_signal` (inside `if`)
424              --> src/main.rs:3:25
425              |
426            3 |         let something = use_signal(|| {
427              |                         ^^^^^^^^^^
428            4 |             "hands"
429            5 |         });
430              |
431              = note: `if you_are_happy && you_know_it { … }` is the conditional
432        "#};
433
434        assert_eq!(expected, issue_report.to_string());
435    }
436}