]> git.lizzy.rs Git - rust.git/blob - crates/ide_assists/src/handlers/remove_dbg.rs
Merge #8436
[rust.git] / crates / ide_assists / src / handlers / remove_dbg.rs
1 use syntax::{
2     ast::{self, AstNode, AstToken},
3     match_ast, SyntaxElement, TextRange, TextSize, T,
4 };
5
6 use crate::{AssistContext, AssistId, AssistKind, Assists};
7
8 // Assist: remove_dbg
9 //
10 // Removes `dbg!()` macro call.
11 //
12 // ```
13 // fn main() {
14 //     $0dbg!(92);
15 // }
16 // ```
17 // ->
18 // ```
19 // fn main() {
20 //     92;
21 // }
22 // ```
23 pub(crate) fn remove_dbg(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
24     let macro_call = ctx.find_node_at_offset::<ast::MacroCall>()?;
25     let new_contents = adjusted_macro_contents(&macro_call)?;
26
27     let parent = macro_call.syntax().parent();
28
29     let macro_text_range = if let Some(it) = parent.as_ref() {
30         if new_contents.is_empty() {
31             match_ast! {
32                 match it {
33                     ast::BlockExpr(_it) => {
34                         macro_call.syntax()
35                             .prev_sibling_or_token()
36                             .and_then(whitespace_start)
37                             .map(|start| TextRange::new(start, macro_call.syntax().text_range().end()))
38                             .unwrap_or(macro_call.syntax().text_range())
39                     },
40                     ast::ExprStmt(it) => {
41                         let start = it
42                             .syntax()
43                             .prev_sibling_or_token()
44                             .and_then(whitespace_start)
45                             .unwrap_or(it.syntax().text_range().start());
46                         let end = it.syntax().text_range().end();
47
48                         TextRange::new(start, end)
49                     },
50                     _ => macro_call.syntax().text_range()
51                 }
52             }
53         } else {
54             macro_call.syntax().text_range()
55         }
56     } else {
57         macro_call.syntax().text_range()
58     };
59
60     let macro_end = if macro_call.semicolon_token().is_some() {
61         macro_text_range.end() - TextSize::of(';')
62     } else {
63         macro_text_range.end()
64     };
65
66     acc.add(
67         AssistId("remove_dbg", AssistKind::Refactor),
68         "Remove dbg!()",
69         macro_text_range,
70         |builder| {
71             builder.replace(
72                 TextRange::new(macro_text_range.start(), macro_end),
73                 if new_contents.is_empty() && parent.and_then(ast::LetStmt::cast).is_some() {
74                     ast::make::expr_unit().to_string()
75                 } else {
76                     new_contents
77                 },
78             );
79         },
80     )
81 }
82
83 fn whitespace_start(it: SyntaxElement) -> Option<TextSize> {
84     Some(it.into_token().and_then(ast::Whitespace::cast)?.syntax().text_range().start())
85 }
86
87 fn adjusted_macro_contents(macro_call: &ast::MacroCall) -> Option<String> {
88     let contents = get_valid_macrocall_contents(&macro_call, "dbg")?;
89     let macro_text_with_brackets = macro_call.token_tree()?.syntax().text();
90     let macro_text_in_brackets = macro_text_with_brackets.slice(TextRange::new(
91         TextSize::of('('),
92         macro_text_with_brackets.len() - TextSize::of(')'),
93     ));
94
95     Some(
96         if !is_leaf_or_control_flow_expr(macro_call)
97             && needs_parentheses_around_macro_contents(contents)
98         {
99             format!("({})", macro_text_in_brackets)
100         } else {
101             macro_text_in_brackets.to_string()
102         },
103     )
104 }
105
106 fn is_leaf_or_control_flow_expr(macro_call: &ast::MacroCall) -> bool {
107     macro_call.syntax().next_sibling().is_none()
108         || match macro_call.syntax().parent() {
109             Some(parent) => match_ast! {
110                 match parent {
111                     ast::Condition(_it) => true,
112                     ast::MatchExpr(_it) => true,
113                     _ => false,
114                 }
115             },
116             None => false,
117         }
118 }
119
120 /// Verifies that the given macro_call actually matches the given name
121 /// and contains proper ending tokens, then returns the contents between the ending tokens
122 fn get_valid_macrocall_contents(
123     macro_call: &ast::MacroCall,
124     macro_name: &str,
125 ) -> Option<Vec<SyntaxElement>> {
126     let path = macro_call.path()?;
127     let name_ref = path.segment()?.name_ref()?;
128
129     // Make sure it is actually a dbg-macro call, dbg followed by !
130     let excl = path.syntax().next_sibling_or_token()?;
131     if name_ref.text() != macro_name || excl.kind() != T![!] {
132         return None;
133     }
134
135     let mut children_with_tokens = macro_call.token_tree()?.syntax().children_with_tokens();
136     let first_child = children_with_tokens.next()?;
137     let mut contents_between_brackets = children_with_tokens.collect::<Vec<_>>();
138     let last_child = contents_between_brackets.pop()?;
139
140     match (first_child.kind(), last_child.kind()) {
141         (T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']) => {
142             Some(contents_between_brackets)
143         }
144         _ => None,
145     }
146 }
147
148 fn needs_parentheses_around_macro_contents(macro_contents: Vec<SyntaxElement>) -> bool {
149     if macro_contents.len() < 2 {
150         return false;
151     }
152     let mut macro_contents = macro_contents.into_iter().peekable();
153     let mut unpaired_brackets_in_contents = Vec::new();
154     while let Some(element) = macro_contents.next() {
155         match element.kind() {
156             T!['('] | T!['['] | T!['{'] => unpaired_brackets_in_contents.push(element),
157             T![')'] => {
158                 if !matches!(unpaired_brackets_in_contents.pop(), Some(correct_bracket) if correct_bracket.kind() == T!['('])
159                 {
160                     return true;
161                 }
162             }
163             T![']'] => {
164                 if !matches!(unpaired_brackets_in_contents.pop(), Some(correct_bracket) if correct_bracket.kind() == T!['['])
165                 {
166                     return true;
167                 }
168             }
169             T!['}'] => {
170                 if !matches!(unpaired_brackets_in_contents.pop(), Some(correct_bracket) if correct_bracket.kind() == T!['{'])
171                 {
172                     return true;
173                 }
174             }
175             symbol_kind => {
176                 let symbol_not_in_bracket = unpaired_brackets_in_contents.is_empty();
177                 if symbol_not_in_bracket
178                     && symbol_kind != T![:] // paths
179                     && (symbol_kind != T![.] // field/method access
180                         || macro_contents // range expressions consist of two SyntaxKind::Dot in macro invocations
181                             .peek()
182                             .map(|element| element.kind() == T![.])
183                             .unwrap_or(false))
184                     && symbol_kind != T![?] // try operator
185                     && (symbol_kind.is_punct() || symbol_kind == T![as])
186                 {
187                     return true;
188                 }
189             }
190         }
191     }
192     !unpaired_brackets_in_contents.is_empty()
193 }
194
195 #[cfg(test)]
196 mod tests {
197     use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
198
199     use super::*;
200
201     #[test]
202     fn test_remove_dbg() {
203         check_assist(remove_dbg, "$0dbg!(1 + 1)", "1 + 1");
204
205         check_assist(remove_dbg, "dbg!$0((1 + 1))", "(1 + 1)");
206
207         check_assist(remove_dbg, "dbg!(1 $0+ 1)", "1 + 1");
208
209         check_assist(remove_dbg, "let _ = $0dbg!(1 + 1)", "let _ = 1 + 1");
210
211         check_assist(
212             remove_dbg,
213             "
214 fn foo(n: usize) {
215     if let Some(_) = dbg!(n.$0checked_sub(4)) {
216         // ...
217     }
218 }
219 ",
220             "
221 fn foo(n: usize) {
222     if let Some(_) = n.checked_sub(4) {
223         // ...
224     }
225 }
226 ",
227         );
228
229         check_assist(remove_dbg, "$0dbg!(Foo::foo_test()).bar()", "Foo::foo_test().bar()");
230     }
231
232     #[test]
233     fn test_remove_dbg_with_brackets_and_braces() {
234         check_assist(remove_dbg, "dbg![$01 + 1]", "1 + 1");
235         check_assist(remove_dbg, "dbg!{$01 + 1}", "1 + 1");
236     }
237
238     #[test]
239     fn test_remove_dbg_not_applicable() {
240         check_assist_not_applicable(remove_dbg, "$0vec![1, 2, 3]");
241         check_assist_not_applicable(remove_dbg, "$0dbg(5, 6, 7)");
242         check_assist_not_applicable(remove_dbg, "$0dbg!(5, 6, 7");
243     }
244
245     #[test]
246     fn test_remove_dbg_target() {
247         check_assist_target(
248             remove_dbg,
249             "
250 fn foo(n: usize) {
251     if let Some(_) = dbg!(n.$0checked_sub(4)) {
252         // ...
253     }
254 }
255 ",
256             "dbg!(n.checked_sub(4))",
257         );
258     }
259
260     #[test]
261     fn test_remove_dbg_keep_semicolon() {
262         // https://github.com/rust-analyzer/rust-analyzer/issues/5129#issuecomment-651399779
263         // not quite though
264         // adding a comment at the end of the line makes
265         // the ast::MacroCall to include the semicolon at the end
266         check_assist(
267             remove_dbg,
268             r#"let res = $0dbg!(1 * 20); // needless comment"#,
269             r#"let res = 1 * 20; // needless comment"#,
270         );
271     }
272
273     #[test]
274     fn remove_dbg_from_non_leaf_simple_expression() {
275         check_assist(
276             remove_dbg,
277             "
278 fn main() {
279     let mut a = 1;
280     while dbg!$0(a) < 10000 {
281         a += 1;
282     }
283 }
284 ",
285             "
286 fn main() {
287     let mut a = 1;
288     while a < 10000 {
289         a += 1;
290     }
291 }
292 ",
293         );
294     }
295
296     #[test]
297     fn test_remove_dbg_keep_expression() {
298         check_assist(
299             remove_dbg,
300             r#"let res = $0dbg!(a + b).foo();"#,
301             r#"let res = (a + b).foo();"#,
302         );
303
304         check_assist(remove_dbg, r#"let res = $0dbg!(2 + 2) * 5"#, r#"let res = (2 + 2) * 5"#);
305         check_assist(remove_dbg, r#"let res = $0dbg![2 + 2] * 5"#, r#"let res = (2 + 2) * 5"#);
306     }
307
308     #[test]
309     fn test_remove_dbg_method_chaining() {
310         check_assist(
311             remove_dbg,
312             r#"let res = $0dbg!(foo().bar()).baz();"#,
313             r#"let res = foo().bar().baz();"#,
314         );
315         check_assist(
316             remove_dbg,
317             r#"let res = $0dbg!(foo.bar()).baz();"#,
318             r#"let res = foo.bar().baz();"#,
319         );
320     }
321
322     #[test]
323     fn test_remove_dbg_field_chaining() {
324         check_assist(remove_dbg, r#"let res = $0dbg!(foo.bar).baz;"#, r#"let res = foo.bar.baz;"#);
325     }
326
327     #[test]
328     fn test_remove_dbg_from_inside_fn() {
329         check_assist_target(
330             remove_dbg,
331             r#"
332 fn square(x: u32) -> u32 {
333     x * x
334 }
335
336 fn main() {
337     let x = square(dbg$0!(5 + 10));
338     println!("{}", x);
339 }"#,
340             "dbg!(5 + 10)",
341         );
342
343         check_assist(
344             remove_dbg,
345             r#"
346 fn square(x: u32) -> u32 {
347     x * x
348 }
349
350 fn main() {
351     let x = square(dbg$0!(5 + 10));
352     println!("{}", x);
353 }"#,
354             r#"
355 fn square(x: u32) -> u32 {
356     x * x
357 }
358
359 fn main() {
360     let x = square(5 + 10);
361     println!("{}", x);
362 }"#,
363         );
364     }
365
366     #[test]
367     fn test_remove_dbg_try_expr() {
368         check_assist(
369             remove_dbg,
370             r#"let res = $0dbg!(result?).foo();"#,
371             r#"let res = result?.foo();"#,
372         );
373     }
374
375     #[test]
376     fn test_remove_dbg_await_expr() {
377         check_assist(
378             remove_dbg,
379             r#"let res = $0dbg!(fut.await).foo();"#,
380             r#"let res = fut.await.foo();"#,
381         );
382     }
383
384     #[test]
385     fn test_remove_dbg_as_cast() {
386         check_assist(
387             remove_dbg,
388             r#"let res = $0dbg!(3 as usize).foo();"#,
389             r#"let res = (3 as usize).foo();"#,
390         );
391     }
392
393     #[test]
394     fn test_remove_dbg_index_expr() {
395         check_assist(
396             remove_dbg,
397             r#"let res = $0dbg!(array[3]).foo();"#,
398             r#"let res = array[3].foo();"#,
399         );
400         check_assist(
401             remove_dbg,
402             r#"let res = $0dbg!(tuple.3).foo();"#,
403             r#"let res = tuple.3.foo();"#,
404         );
405     }
406
407     #[test]
408     fn test_remove_dbg_range_expr() {
409         check_assist(
410             remove_dbg,
411             r#"let res = $0dbg!(foo..bar).foo();"#,
412             r#"let res = (foo..bar).foo();"#,
413         );
414         check_assist(
415             remove_dbg,
416             r#"let res = $0dbg!(foo..=bar).foo();"#,
417             r#"let res = (foo..=bar).foo();"#,
418         );
419     }
420
421     #[test]
422     fn test_remove_dbg_followed_by_block() {
423         check_assist(
424             remove_dbg,
425             r#"fn foo() {
426     if $0dbg!(x || y) {}
427 }"#,
428             r#"fn foo() {
429     if x || y {}
430 }"#,
431         );
432         check_assist(
433             remove_dbg,
434             r#"fn foo() {
435     while let foo = $0dbg!(&x) {}
436 }"#,
437             r#"fn foo() {
438     while let foo = &x {}
439 }"#,
440         );
441         check_assist(
442             remove_dbg,
443             r#"fn foo() {
444     if let foo = $0dbg!(&x) {}
445 }"#,
446             r#"fn foo() {
447     if let foo = &x {}
448 }"#,
449         );
450         check_assist(
451             remove_dbg,
452             r#"fn foo() {
453     match $0dbg!(&x) {}
454 }"#,
455             r#"fn foo() {
456     match &x {}
457 }"#,
458         );
459     }
460
461     #[test]
462     fn test_remove_empty_dbg() {
463         check_assist(remove_dbg, r#"fn foo() { $0dbg!(); }"#, r#"fn foo() { }"#);
464         check_assist(
465             remove_dbg,
466             r#"
467 fn foo() {
468     $0dbg!();
469 }
470 "#,
471             r#"
472 fn foo() {
473 }
474 "#,
475         );
476         check_assist(
477             remove_dbg,
478             r#"
479 fn foo() {
480     let test = $0dbg!();
481 }"#,
482             r#"
483 fn foo() {
484     let test = ();
485 }"#,
486         );
487         check_assist(
488             remove_dbg,
489             r#"
490 fn foo() {
491     let t = {
492         println!("Hello, world");
493         $0dbg!()
494     };
495 }"#,
496             r#"
497 fn foo() {
498     let t = {
499         println!("Hello, world");
500     };
501 }"#,
502         );
503     }
504 }