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
15pub 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)] pub enum Issue {
165 HookInsideConditional(HookInfo, ConditionalInfo),
167 HookInsideLoop(HookInfo, AnyLoopInfo),
169 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}