Skip to main content

ide_assists/handlers/
remove_dbg.rs

1use itertools::Itertools;
2use syntax::{
3    Edition, NodeOrToken, SyntaxNode, SyntaxToken, T,
4    ast::{self, AstNode, syntax_factory::SyntaxFactory},
5    match_ast,
6    syntax_editor::{Position, SyntaxEditor},
7};
8
9use crate::{AssistContext, AssistId, Assists};
10
11// Assist: remove_dbg
12//
13// Removes `dbg!()` macro call.
14//
15// ```
16// fn main() {
17//     let x = $0dbg!(42 * dbg!(4 + 2));$0
18// }
19// ```
20// ->
21// ```
22// fn main() {
23//     let x = 42 * (4 + 2);
24// }
25// ```
26pub(crate) fn remove_dbg(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
27    let (editor, _) = SyntaxEditor::new(ctx.source_file().syntax().clone());
28    let make = editor.make();
29    let macro_calls = if ctx.has_empty_selection() {
30        vec![ctx.find_node_at_offset::<ast::MacroExpr>()?]
31    } else {
32        ctx.covering_element()
33            .as_node()?
34            .descendants()
35            .filter(|node| ctx.selection_trimmed().contains_range(node.text_range()))
36            // When the selection exactly covers the macro call to be removed, `covering_element()`
37            // returns `ast::MacroCall` instead of its parent `ast::MacroExpr` that we want. So
38            // first try finding `ast::MacroCall`s and then retrieve their parent.
39            .filter_map(ast::MacroCall::cast)
40            .filter_map(|it| it.syntax().parent().and_then(ast::MacroExpr::cast))
41            .collect()
42    };
43
44    let replacements = macro_calls
45        .into_iter()
46        .filter_map(|macro_expr| compute_dbg_replacement(macro_expr, make))
47        .collect::<Vec<_>>();
48    let target = replacements
49        .iter()
50        .flat_map(|(node_or_token, _)| node_or_token.iter())
51        .map(|t| t.text_range())
52        .reduce(|acc, range| acc.cover(range))?;
53    acc.add(AssistId::quick_fix("remove_dbg"), "Remove dbg!()", target, |builder| {
54        for (range, expr) in replacements {
55            if let Some(expr) = expr {
56                editor.insert(Position::before(range[0].clone()), expr.syntax());
57            }
58            for node_or_token in range {
59                editor.delete(node_or_token);
60            }
61        }
62        builder.add_file_edits(ctx.vfs_file_id(), editor);
63    })
64}
65
66/// Returns `None` when either
67/// - macro call is not `dbg!()`
68/// - any node inside `dbg!()` could not be parsed as an expression
69/// - (`macro_expr` has no parent - is that possible?)
70///
71/// Returns `Some(_, None)` when the macro call should just be removed.
72fn compute_dbg_replacement(
73    macro_expr: ast::MacroExpr,
74    make: &SyntaxFactory,
75) -> Option<(Vec<NodeOrToken<SyntaxNode, SyntaxToken>>, Option<ast::Expr>)> {
76    let macro_call = macro_expr.macro_call()?;
77    let tt = macro_call.token_tree()?;
78    let r_delim = NodeOrToken::Token(tt.right_delimiter_token()?);
79    if macro_call.path()?.segment()?.name_ref()?.text() != "dbg"
80        || macro_call.excl_token().is_none()
81    {
82        return None;
83    }
84
85    let mac_input = tt.syntax().children_with_tokens().skip(1).take_while(|it| *it != r_delim);
86    let input_expressions = mac_input.chunk_by(|tok| tok.kind() == T![,]);
87    let input_expressions = input_expressions
88        .into_iter()
89        .filter_map(|(is_sep, group)| (!is_sep).then_some(group))
90        .map(|tokens| tokens.collect::<Vec<_>>())
91        .filter(|tokens| !tokens.iter().all(|it| it.kind().is_trivia()))
92        .map(|tokens| syntax::hacks::parse_expr_from_str(&tokens.iter().join(""), Edition::CURRENT))
93        .collect::<Option<Vec<ast::Expr>>>()?;
94
95    let parent = macro_expr.syntax().parent()?;
96    Some(match &*input_expressions {
97        // dbg!()
98        [] => {
99            match_ast! {
100                match parent {
101                    ast::StmtList(_) => {
102                        let mut replace = vec![macro_expr.syntax().clone().into()];
103                        if let Some(prev_sibling) = macro_expr.syntax().prev_sibling_or_token()
104                            && prev_sibling.kind() == syntax::SyntaxKind::WHITESPACE {
105                                replace.push(prev_sibling);
106                        }
107                        (replace, None)
108                    },
109                    ast::ExprStmt(it) => {
110                        let mut replace = vec![it.syntax().clone().into()];
111                        if let Some(prev_sibling) = it.syntax().prev_sibling_or_token()
112                            && prev_sibling.kind() == syntax::SyntaxKind::WHITESPACE {
113                                replace.push(prev_sibling);
114                        }
115                        (replace, None)
116                    },
117                    _ => (vec![macro_call.syntax().clone().into()], Some(make.expr_unit())),
118                }
119            }
120        }
121        // dbg!(2, 'x', &x, x, ...);
122        exprs if ast::ExprStmt::can_cast(parent.kind()) && exprs.iter().all(pure_expr) => {
123            let mut replace = vec![parent.clone().into()];
124            if let Some(prev_sibling) = parent.prev_sibling_or_token()
125                && prev_sibling.kind() == syntax::SyntaxKind::WHITESPACE
126            {
127                replace.push(prev_sibling);
128            }
129            (replace, None)
130        }
131        // dbg!(expr0)
132        [expr] => {
133            // dbg!(expr, &parent);
134            let wrap = match ast::Expr::cast(parent) {
135                Some(parent) => match (expr, parent) {
136                    (ast::Expr::CastExpr(_), ast::Expr::CastExpr(_)) => false,
137                    (
138                        ast::Expr::PrefixExpr(_) | ast::Expr::RefExpr(_) | ast::Expr::MacroExpr(_),
139                        ast::Expr::AwaitExpr(_)
140                        | ast::Expr::CallExpr(_)
141                        | ast::Expr::CastExpr(_)
142                        | ast::Expr::FieldExpr(_)
143                        | ast::Expr::IndexExpr(_)
144                        | ast::Expr::MethodCallExpr(_)
145                        | ast::Expr::RangeExpr(_)
146                        | ast::Expr::TryExpr(_),
147                    ) => true,
148                    (
149                        ast::Expr::BinExpr(_)
150                        | ast::Expr::CastExpr(_)
151                        | ast::Expr::RangeExpr(_)
152                        | ast::Expr::MacroExpr(_),
153                        ast::Expr::AwaitExpr(_)
154                        | ast::Expr::BinExpr(_)
155                        | ast::Expr::CallExpr(_)
156                        | ast::Expr::CastExpr(_)
157                        | ast::Expr::FieldExpr(_)
158                        | ast::Expr::IndexExpr(_)
159                        | ast::Expr::MethodCallExpr(_)
160                        | ast::Expr::PrefixExpr(_)
161                        | ast::Expr::RangeExpr(_)
162                        | ast::Expr::RefExpr(_)
163                        | ast::Expr::TryExpr(_),
164                    ) => true,
165                    _ => false,
166                },
167                None => false,
168            };
169            let expr = replace_nested_dbgs(expr.clone(), make);
170            let expr = if wrap { make.expr_paren(expr).into() } else { expr };
171            (vec![macro_call.syntax().clone().into()], Some(expr))
172        }
173        // dbg!(expr0, expr1, ...)
174        exprs => {
175            let exprs = exprs.iter().cloned().map(|expr| replace_nested_dbgs(expr, make));
176            let expr = make.expr_tuple(exprs);
177            (vec![macro_call.syntax().clone().into()], Some(expr.into()))
178        }
179    })
180}
181
182fn pure_expr(expr: &ast::Expr) -> bool {
183    match_ast! {
184        match (expr.syntax()) {
185            ast::Literal(_) => true,
186            ast::RefExpr(it) => {
187                matches!(it.expr(), Some(ast::Expr::PathExpr(p))
188                    if p.path().and_then(|p| p.as_single_name_ref()).is_some())
189            },
190            ast::PathExpr(it) => it.path().and_then(|it| it.as_single_name_ref()).is_some(),
191            _ => false,
192        }
193    }
194}
195
196fn replace_nested_dbgs(expanded: ast::Expr, make: &SyntaxFactory) -> ast::Expr {
197    if let ast::Expr::MacroExpr(mac) = &expanded {
198        // Special-case when `expanded` itself is `dbg!()` since we cannot replace the whole tree
199        // with `ted`. It should be fairly rare as it means the user wrote `dbg!(dbg!(..))` but you
200        // never know how code ends up being!
201        let replaced = if let Some((_, expr_opt)) = compute_dbg_replacement(mac.clone(), make) {
202            match expr_opt {
203                Some(expr) => expr,
204                None => {
205                    stdx::never!("dbg! inside dbg! should not be just removed");
206                    expanded
207                }
208            }
209        } else {
210            expanded
211        };
212
213        return replaced;
214    }
215
216    let (editor, expanded) = SyntaxEditor::with_ast_node(&expanded);
217    // We need to collect to avoid mutation during traversal.
218    let macro_exprs: Vec<_> =
219        expanded.syntax().descendants().filter_map(ast::MacroExpr::cast).collect();
220
221    for mac in macro_exprs {
222        let expr_opt = match compute_dbg_replacement(mac.clone(), make) {
223            Some((_, expr)) => expr,
224            None => continue,
225        };
226
227        if let Some(expr) = expr_opt {
228            editor.replace(mac.syntax(), expr.syntax());
229        } else {
230            editor.delete(mac.syntax());
231        }
232    }
233    let expanded_syntax = editor.finish().new_root().clone();
234    ast::Expr::cast(expanded_syntax).unwrap()
235}
236
237#[cfg(test)]
238mod tests {
239    use crate::tests::{check_assist, check_assist_not_applicable};
240
241    use super::*;
242
243    fn check(
244        #[rust_analyzer::rust_fixture] ra_fixture_before: &str,
245        #[rust_analyzer::rust_fixture] ra_fixture_after: &str,
246    ) {
247        check_assist(
248            remove_dbg,
249            &format!("fn main() {{\n{ra_fixture_before}\n}}"),
250            &format!("fn main() {{\n{ra_fixture_after}\n}}"),
251        );
252    }
253
254    #[test]
255    fn test_remove_dbg() {
256        check("$0dbg!(1 + 1)", "1 + 1");
257        check("dbg!$0(1 + 1)", "1 + 1");
258        check("dbg!(1 $0+ 1)", "1 + 1");
259        check("dbg![$01 + 1]", "1 + 1");
260        check("dbg!{$01 + 1}", "1 + 1");
261    }
262
263    #[test]
264    fn test_remove_simple_dbg_statement() {
265        check_assist(
266            remove_dbg,
267            r#"
268fn foo() {
269    let n = 2;
270    $0dbg!(3);
271    dbg!(2.6);
272    dbg!(1, 2.5);
273    dbg!('x');
274    dbg!(&n);
275    dbg!(n);
276    dbg!(n,);
277    dbg!(n, );
278    // needless comment
279    dbg!("foo");$0
280}
281"#,
282            r#"
283fn foo() {
284    let n = 2;
285    // needless comment
286}
287"#,
288        );
289    }
290
291    #[test]
292    fn test_remove_trailing_comma_dbg() {
293        check("$0dbg!(1 + 1,)", "1 + 1");
294        check("$0dbg!(1 + 1, )", "1 + 1");
295        check("$0dbg!(1 + 1,\n)", "1 + 1");
296        check("$0dbg!(1 + 1, 2 + 3)", "(1 + 1, 2 + 3)");
297        check("$0dbg!(1 + 1, 2 + 3 )", "(1 + 1, 2 + 3)");
298        check("$0dbg!(1 + 1, 2 + 3, )", "(1 + 1, 2 + 3)");
299        check("$0dbg!(1 + 1, 2 + 3 ,)", "(1 + 1, 2 + 3)");
300    }
301
302    #[test]
303    fn test_remove_dbg_not_applicable() {
304        check_assist_not_applicable(remove_dbg, "fn main() {$0vec![1, 2, 3]}");
305        check_assist_not_applicable(remove_dbg, "fn main() {$0dbg(5, 6, 7)}");
306        check_assist_not_applicable(remove_dbg, "fn main() {$0dbg!(5, 6, 7}");
307    }
308
309    #[test]
310    fn test_remove_dbg_keep_semicolon_in_let() {
311        // https://github.com/rust-lang/rust-analyzer/issues/5129#issuecomment-651399779
312        check(
313            r#"let res = $0dbg!(1 * 20); // needless comment"#,
314            r#"let res = 1 * 20; // needless comment"#,
315        );
316        check(r#"let res = $0dbg!(); // needless comment"#, r#"let res = (); // needless comment"#);
317        check(
318            r#"let res = $0dbg!(1, 2); // needless comment"#,
319            r#"let res = (1, 2); // needless comment"#,
320        );
321    }
322
323    #[test]
324    fn test_remove_dbg_cast_cast() {
325        check(r#"let res = $0dbg!(x as u32) as u32;"#, r#"let res = x as u32 as u32;"#);
326    }
327
328    #[test]
329    fn test_remove_dbg_prefix() {
330        check(r#"let res = $0dbg!(&result).foo();"#, r#"let res = (&result).foo();"#);
331        check(r#"let res = &$0dbg!(&result);"#, r#"let res = &&result;"#);
332        check(r#"let res = $0dbg!(!result) && true;"#, r#"let res = !result && true;"#);
333    }
334
335    #[test]
336    fn test_remove_dbg_post_expr() {
337        check(r#"let res = $0dbg!(fut.await).foo();"#, r#"let res = fut.await.foo();"#);
338        check(r#"let res = $0dbg!(result?).foo();"#, r#"let res = result?.foo();"#);
339        check(r#"let res = $0dbg!(foo as u32).foo();"#, r#"let res = (foo as u32).foo();"#);
340        check(r#"let res = $0dbg!(array[3]).foo();"#, r#"let res = array[3].foo();"#);
341        check(r#"let res = $0dbg!(tuple.3).foo();"#, r#"let res = tuple.3.foo();"#);
342    }
343
344    #[test]
345    fn test_remove_dbg_range_expr() {
346        check(r#"let res = $0dbg!(foo..bar).foo();"#, r#"let res = (foo..bar).foo();"#);
347        check(r#"let res = $0dbg!(foo..=bar).foo();"#, r#"let res = (foo..=bar).foo();"#);
348    }
349
350    #[test]
351    fn test_remove_empty_dbg() {
352        check_assist(remove_dbg, r#"fn foo() { $0dbg!(); }"#, r#"fn foo() { }"#);
353        check_assist(
354            remove_dbg,
355            r#"
356fn foo() {
357    $0dbg!();
358}
359"#,
360            r#"
361fn foo() {
362}
363"#,
364        );
365        check_assist(
366            remove_dbg,
367            r#"
368fn foo() {
369    let test = $0dbg!();
370}"#,
371            r#"
372fn foo() {
373    let test = ();
374}"#,
375        );
376        check_assist(
377            remove_dbg,
378            r#"
379fn foo() {
380    let t = {
381        println!("Hello, world");
382        $0dbg!()
383    };
384}"#,
385            r#"
386fn foo() {
387    let t = {
388        println!("Hello, world");
389    };
390}"#,
391        );
392    }
393
394    #[test]
395    fn test_remove_multi_dbg() {
396        check(r#"$0dbg!(0, 1)"#, r#"(0, 1)"#);
397        check(r#"$0dbg!(0, (1, 2))"#, r#"(0, (1, 2))"#);
398    }
399
400    #[test]
401    fn test_range() {
402        check(
403            r#"
404fn f() {
405    dbg!(0) + $0dbg!(1);
406    dbg!(())$0
407}
408"#,
409            r#"
410fn f() {
411    dbg!(0) + 1;
412    ()
413}
414"#,
415        );
416    }
417
418    #[test]
419    fn test_range_partial() {
420        check_assist_not_applicable(remove_dbg, r#"$0dbg$0!(0)"#);
421        check_assist_not_applicable(remove_dbg, r#"$0dbg!(0$0)"#);
422    }
423
424    #[test]
425    fn test_nested_dbg() {
426        check(
427            r#"$0let x = dbg!(dbg!(dbg!(dbg!(0 + 1)) * 2) + dbg!(3));$0"#,
428            r#"let x = ((0 + 1) * 2) + 3;"#,
429        );
430        check(r#"$0dbg!(10, dbg!(), dbg!(20, 30))$0"#, r#"(10, (), (20, 30))"#);
431    }
432
433    #[test]
434    fn test_multiple_nested_dbg() {
435        check(
436            r#"
437fn f() {
438    $0dbg!();
439    let x = dbg!(dbg!(dbg!(0 + 1)) + 2) + dbg!(3);
440    dbg!(10, dbg!(), dbg!(20, 30));$0
441}
442"#,
443            r#"
444fn f() {
445    let x = ((0 + 1) + 2) + 3;
446    (10, (), (20, 30));
447}
448"#,
449        );
450    }
451}