Skip to main content

ide_assists/handlers/
replace_let_with_if_let.rs

1use ide_db::ty_filter::TryEnum;
2use syntax::{
3    AstNode, T,
4    ast::{
5        self,
6        edit::{AstNodeEdit, IndentLevel},
7        syntax_factory::SyntaxFactory,
8    },
9};
10
11use crate::{AssistContext, AssistId, Assists};
12
13// Assist: replace_let_with_if_let
14//
15// Replaces `let` with an `if let`.
16//
17// ```
18// # enum Option<T> { Some(T), None }
19//
20// fn main(action: Action) {
21//     $0let x = compute();
22// }
23//
24// fn compute() -> Option<i32> { None }
25// ```
26// ->
27// ```
28// # enum Option<T> { Some(T), None }
29//
30// fn main(action: Action) {
31//     if let Some(x) = compute() {
32//     }
33// }
34//
35// fn compute() -> Option<i32> { None }
36// ```
37pub(crate) fn replace_let_with_if_let(
38    acc: &mut Assists,
39    ctx: &AssistContext<'_, '_>,
40) -> Option<()> {
41    let let_kw = ctx.find_token_syntax_at_offset(T![let])?;
42    let let_stmt = let_kw.parent().and_then(ast::LetStmt::cast)?;
43    let init = let_stmt.initializer()?;
44    let original_pat = let_stmt.pat()?;
45
46    let target = let_kw.text_range();
47    acc.add(
48        AssistId::refactor_rewrite("replace_let_with_if_let"),
49        "Replace let with if let",
50        target,
51        |builder| {
52            let editor = builder.make_editor(let_stmt.syntax());
53            let make = editor.make();
54            let ty = ctx.sema.type_of_expr(&init);
55            let pat = if let_stmt.let_else().is_some() {
56                // Do not add the wrapper type that implements `Try`,
57                // since the statement already wraps the pattern.
58                original_pat
59            } else {
60                let happy_variant = ty
61                    .and_then(|ty| TryEnum::from_ty(&ctx.sema, &ty.adjusted()))
62                    .map(|it| it.happy_case());
63                match happy_variant {
64                    None => original_pat,
65                    Some(var_name) => {
66                        make.tuple_struct_pat(make.ident_path(var_name), [original_pat]).into()
67                    }
68                }
69            };
70            let init_expr =
71                if let_expr_needs_paren(&init) { make.expr_paren(init).into() } else { init };
72
73            let block = make.block_expr([], None);
74            let block = block.indent(IndentLevel::from_node(let_stmt.syntax()));
75            let if_expr = make.expr_if(
76                make.expr_let(pat, init_expr).into(),
77                block,
78                let_stmt
79                    .let_else()
80                    .and_then(|let_else| let_else.block_expr().map(ast::ElseBranch::from)),
81            );
82            let if_stmt = make.expr_stmt(if_expr.into());
83
84            editor.replace(let_stmt.syntax(), if_stmt.syntax());
85            builder.add_file_edits(ctx.vfs_file_id(), editor);
86        },
87    )
88}
89
90fn let_expr_needs_paren(expr: &ast::Expr) -> bool {
91    let make = SyntaxFactory::without_mappings();
92    let fake_expr_let = make.expr_let(make.tuple_pat(None).into(), make.expr_unit());
93    let fake_if = make.expr_if(fake_expr_let.into(), make.expr_empty_block(), None);
94    let Some(ast::Expr::LetExpr(fake_expr_let)) = fake_if.condition() else {
95        stdx::never!();
96        return false;
97    };
98    let Some(fake_expr) = fake_expr_let.expr() else {
99        stdx::never!();
100        return false;
101    };
102    expr.needs_parens_in_place_of(fake_expr_let.syntax(), fake_expr.syntax())
103}
104
105#[cfg(test)]
106mod tests {
107    use crate::tests::check_assist;
108
109    use super::*;
110
111    #[test]
112    fn replace_let_try_enum_ref() {
113        check_assist(
114            replace_let_with_if_let,
115            r"
116//- minicore: option
117fn main(action: Action) {
118    $0let x = compute();
119}
120
121fn compute() -> &'static Option<i32> { &None }
122            ",
123            r"
124fn main(action: Action) {
125    if let Some(x) = compute() {
126    }
127}
128
129fn compute() -> &'static Option<i32> { &None }
130            ",
131        )
132    }
133
134    #[test]
135    fn replace_let_unknown_enum() {
136        check_assist(
137            replace_let_with_if_let,
138            r"
139enum E<T> { X(T), Y(T) }
140
141fn main() {
142    $0let x = E::X(92);
143}
144            ",
145            r"
146enum E<T> { X(T), Y(T) }
147
148fn main() {
149    if let x = E::X(92) {
150    }
151}
152            ",
153        )
154    }
155
156    #[test]
157    fn replace_let_logic_and() {
158        check_assist(
159            replace_let_with_if_let,
160            r"
161fn main() {
162    $0let x = true && false;
163}
164            ",
165            r"
166fn main() {
167    if let x = (true && false) {
168    }
169}
170            ",
171        )
172    }
173
174    #[test]
175    fn replace_let_logic_or() {
176        check_assist(
177            replace_let_with_if_let,
178            r"
179fn main() {
180    $0let x = true || false;
181}
182            ",
183            r"
184fn main() {
185    if let x = (true || false) {
186    }
187}
188            ",
189        )
190    }
191
192    #[test]
193    fn replace_let_record_expr() {
194        check_assist(
195            replace_let_with_if_let,
196            r"
197fn main() {
198    $0let x = Foo { x };
199}
200            ",
201            r"
202fn main() {
203    if let x = (Foo { x }) {
204    }
205}
206            ",
207        )
208    }
209
210    #[test]
211    fn replace_let_else() {
212        check_assist(
213            replace_let_with_if_let,
214            r"
215//- minicore: option
216fn main() {
217    let a = Some(1);
218    $0let Some(_) = a else { unreachable!() };
219}
220            ",
221            r"
222fn main() {
223    let a = Some(1);
224    if let Some(_) = a {
225    } else { unreachable!() }
226}
227            ",
228        )
229    }
230}