Skip to main content

ide_assists/handlers/
replace_is_method_with_if_let_method.rs

1use either::Either;
2use ide_db::syntax_helpers::suggest_name;
3use syntax::{
4    ast::{self, AstNode, HasArgList, prec::ExprPrecedence, syntax_factory::SyntaxFactory},
5    syntax_editor::SyntaxEditor,
6};
7
8use crate::{
9    AssistContext, AssistId, Assists,
10    utils::{cover_let_chain, wrap_paren, wrap_paren_in_call},
11};
12
13// Assist: replace_is_some_with_if_let_some
14//
15// Replace `if x.is_some()` with `if let Some(_tmp) = x` or `if x.is_ok()` with `if let Ok(_tmp) = x`.
16//
17// ```
18// fn main() {
19//     let x = Some(1);
20//     if x.is_som$0e() {}
21// }
22// ```
23// ->
24// ```
25// fn main() {
26//     let x = Some(1);
27//     if let Some(${0:x1}) = x {}
28// }
29// ```
30pub(crate) fn replace_is_method_with_if_let_method(
31    acc: &mut Assists,
32    ctx: &AssistContext<'_, '_>,
33) -> Option<()> {
34    let has_cond = ctx.find_node_at_offset::<Either<ast::IfExpr, ast::WhileExpr>>()?;
35
36    let cond = either::for_both!(&has_cond, it => it.condition())?;
37    let cond = cover_let_chain(cond, ctx.selection_trimmed())?;
38    let call_expr = match cond {
39        ast::Expr::MethodCallExpr(call) => call,
40        _ => return None,
41    };
42
43    let token = call_expr.name_ref()?.ident_token()?;
44    let method_kind = token.text().strip_suffix("_and").unwrap_or(token.text());
45    match method_kind {
46        "is_some" | "is_ok" => {
47            let (editor, _) = SyntaxEditor::new(ctx.source_file().syntax().clone());
48            let make = editor.make();
49            let receiver = call_expr.receiver()?;
50            let mut name_generator = suggest_name::NameGenerator::new_from_scope_locals(
51                ctx.sema.scope(has_cond.syntax()),
52            );
53            let var_name = if let ast::Expr::PathExpr(path_expr) = receiver.clone() {
54                name_generator.suggest_name(&path_expr.path()?.to_string())
55            } else {
56                name_generator.for_variable(&receiver, &ctx.sema)
57            };
58            let (pat, predicate) = method_predicate(&call_expr, &var_name, make);
59
60            let (assist_id, message, text) = if method_kind == "is_some" {
61                ("replace_is_some_with_if_let_some", "Replace `is_some` with `let Some`", "Some")
62            } else {
63                ("replace_is_ok_with_if_let_ok", "Replace `is_ok` with `let Ok`", "Ok")
64            };
65
66            acc.add(
67                AssistId::refactor_rewrite(assist_id),
68                message,
69                call_expr.syntax().text_range(),
70                |edit| {
71                    let make = editor.make();
72                    let pat = make.tuple_struct_pat(make.ident_path(text), [pat]).into();
73                    let let_expr = make.expr_let(pat, receiver);
74
75                    if let Some(cap) = ctx.config.snippet_cap
76                        && let Some(ast::Pat::TupleStructPat(pat)) = let_expr.pat()
77                        && let Some(first_var) = pat.fields().next()
78                        && predicate.is_none()
79                    {
80                        let placeholder = edit.make_placeholder_snippet(cap);
81                        editor.add_annotation(first_var.syntax(), placeholder);
82                    }
83
84                    let new_expr = if let Some(predicate) = predicate {
85                        let op = ast::BinaryOp::LogicOp(ast::LogicOp::And);
86                        let predicate = wrap_paren(predicate, make, ExprPrecedence::LAnd);
87                        make.expr_bin(let_expr.into(), op, predicate).into()
88                    } else {
89                        ast::Expr::from(let_expr)
90                    };
91                    editor.replace(call_expr.syntax(), new_expr.syntax());
92                    edit.add_file_edits(ctx.vfs_file_id(), editor);
93                },
94            )
95        }
96        _ => None,
97    }
98}
99
100fn method_predicate(
101    call_expr: &ast::MethodCallExpr,
102    name: &str,
103    make: &SyntaxFactory,
104) -> (ast::Pat, Option<ast::Expr>) {
105    let argument = call_expr.arg_list().and_then(|it| it.args().next());
106    if let Some(ast::Expr::ClosureExpr(it)) = argument.clone()
107        && let Some(pat) = it.param_list().and_then(|it| it.params().next()?.pat())
108    {
109        (pat, it.body())
110    } else {
111        let pat = make.ident_pat(false, false, make.name(name));
112        let expr = argument.map(|expr| {
113            let arg_list = make.arg_list([make.expr_path(make.ident_path(name))]);
114            make.expr_call(wrap_paren_in_call(expr, make), arg_list).into()
115        });
116        (pat.into(), expr)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::tests::{check_assist, check_assist_not_applicable};
123
124    use super::replace_is_method_with_if_let_method;
125
126    #[test]
127    fn replace_is_some_with_if_let_some_works() {
128        check_assist(
129            replace_is_method_with_if_let_method,
130            r#"
131fn main() {
132    let x = Some(1);
133    if x.is_som$0e() {}
134}
135"#,
136            r#"
137fn main() {
138    let x = Some(1);
139    if let Some(${0:x1}) = x {}
140}
141"#,
142        );
143
144        check_assist(
145            replace_is_method_with_if_let_method,
146            r#"
147fn test() -> Option<i32> {
148    Some(1)
149}
150fn main() {
151    if test().is_som$0e() {}
152}
153"#,
154            r#"
155fn test() -> Option<i32> {
156    Some(1)
157}
158fn main() {
159    if let Some(${0:test}) = test() {}
160}
161"#,
162        );
163    }
164
165    #[test]
166    fn replace_is_some_with_if_let_some_not_applicable() {
167        check_assist_not_applicable(
168            replace_is_method_with_if_let_method,
169            r#"
170fn main() {
171    let x = Some(1);
172    if x.is_non$0e() {}
173}
174"#,
175        );
176    }
177
178    #[test]
179    fn replace_is_ok_with_if_let_ok_works() {
180        check_assist(
181            replace_is_method_with_if_let_method,
182            r#"
183fn main() {
184    let x = Ok(1);
185    if x.is_o$0k() {}
186}
187"#,
188            r#"
189fn main() {
190    let x = Ok(1);
191    if let Ok(${0:x1}) = x {}
192}
193"#,
194        );
195
196        check_assist(
197            replace_is_method_with_if_let_method,
198            r#"
199fn test() -> Result<i32> {
200    Ok(1)
201}
202fn main() {
203    if test().is_o$0k() {}
204}
205"#,
206            r#"
207fn test() -> Result<i32> {
208    Ok(1)
209}
210fn main() {
211    if let Ok(${0:test}) = test() {}
212}
213"#,
214        );
215    }
216
217    #[test]
218    fn replace_is_ok_with_if_let_ok_not_applicable() {
219        check_assist_not_applicable(
220            replace_is_method_with_if_let_method,
221            r#"
222fn main() {
223    let x = Ok(1);
224    if x.is_e$0rr() {}
225}
226"#,
227        );
228    }
229
230    #[test]
231    fn replace_is_some_and_with_if_let_chain_some_works() {
232        check_assist(
233            replace_is_method_with_if_let_method,
234            r#"
235fn main() {
236    let x = Some(1);
237    if x.is_som$0e_and(|it| it != 3) {}
238}
239"#,
240            r#"
241fn main() {
242    let x = Some(1);
243    if let Some(it) = x && it != 3 {}
244}
245"#,
246        );
247
248        check_assist(
249            replace_is_method_with_if_let_method,
250            r#"
251fn main() {
252    let x = Some(1);
253    if x.is_som$0e_and(|it| it != 3 || it > 10) {}
254}
255"#,
256            r#"
257fn main() {
258    let x = Some(1);
259    if let Some(it) = x && (it != 3 || it > 10) {}
260}
261"#,
262        );
263
264        check_assist(
265            replace_is_method_with_if_let_method,
266            r#"
267fn main() {
268    let x = Some(1);
269    if x.is_som$0e_and(predicate) {}
270}
271"#,
272            r#"
273fn main() {
274    let x = Some(1);
275    if let Some(x1) = x && predicate(x1) {}
276}
277"#,
278        );
279
280        check_assist(
281            replace_is_method_with_if_let_method,
282            r#"
283fn main() {
284    let x = Some(1);
285    if x.is_som$0e_and(func.f) {}
286}
287"#,
288            r#"
289fn main() {
290    let x = Some(1);
291    if let Some(x1) = x && (func.f)(x1) {}
292}
293"#,
294        );
295    }
296
297    #[test]
298    fn replace_is_some_with_if_let_some_in_let_chain() {
299        check_assist(
300            replace_is_method_with_if_let_method,
301            r#"
302fn main() {
303    let x = Some(1);
304    let cond = true;
305    if cond && x.is_som$0e() {}
306}
307"#,
308            r#"
309fn main() {
310    let x = Some(1);
311    let cond = true;
312    if cond && let Some(${0:x1}) = x {}
313}
314"#,
315        );
316
317        check_assist(
318            replace_is_method_with_if_let_method,
319            r#"
320fn main() {
321    let x = Some(1);
322    let cond = true;
323    if x.is_som$0e() && cond {}
324}
325"#,
326            r#"
327fn main() {
328    let x = Some(1);
329    let cond = true;
330    if let Some(${0:x1}) = x && cond {}
331}
332"#,
333        );
334
335        check_assist(
336            replace_is_method_with_if_let_method,
337            r#"
338fn main() {
339    let x = Some(1);
340    let cond = true;
341    if cond && x.is_som$0e() && cond {}
342}
343"#,
344            r#"
345fn main() {
346    let x = Some(1);
347    let cond = true;
348    if cond && let Some(${0:x1}) = x && cond {}
349}
350"#,
351        );
352    }
353
354    #[test]
355    fn replace_is_some_with_while_let_some() {
356        check_assist(
357            replace_is_method_with_if_let_method,
358            r#"
359fn main() {
360    let mut x = Some(1);
361    while x.is_som$0e() { x = None }
362}
363"#,
364            r#"
365fn main() {
366    let mut x = Some(1);
367    while let Some(${0:x1}) = x { x = None }
368}
369"#,
370        );
371    }
372
373    #[test]
374    fn replace_is_some_with_if_let_some_not_applicable_after_l_curly() {
375        check_assist_not_applicable(
376            replace_is_method_with_if_let_method,
377            r#"
378fn main() {
379    let x = Some(1);
380    if x.is_some() {
381        ()$0
382    }
383}
384"#,
385        );
386    }
387}